chore: consolidate all in-progress parallel-session WIP
audit / bun-audit (push) Successful in 27s
windows-drivers / probe-and-proto (push) Successful in 49s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m10s
apple / swift (push) Successful in 1m18s
decky / build-publish (push) Successful in 34s
windows-drivers / driver-build (push) Successful in 1m37s
audit / cargo-audit (push) Successful in 3m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 42s
ci / bench (push) Successful in 5m56s
windows-host / package (push) Failing after 5m17s
apple / screenshots (push) Successful in 4m45s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11m3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8m6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m15s
arch / build-publish (push) Successful in 16m50s
android / android (push) Successful in 17m7s
docker / deploy-docs (push) Successful in 28s
deb / build-publish (push) Successful in 17m6s
ci / rust (push) Failing after 19m1s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m3s
flatpak / build-publish (push) Successful in 8m0s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m29s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m14s
release / apple (push) Successful in 5m44s
audit / bun-audit (push) Successful in 27s
windows-drivers / probe-and-proto (push) Successful in 49s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m10s
apple / swift (push) Successful in 1m18s
decky / build-publish (push) Successful in 34s
windows-drivers / driver-build (push) Successful in 1m37s
audit / cargo-audit (push) Successful in 3m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 42s
ci / bench (push) Successful in 5m56s
windows-host / package (push) Failing after 5m17s
apple / screenshots (push) Successful in 4m45s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11m3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8m6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m15s
arch / build-publish (push) Successful in 16m50s
android / android (push) Successful in 17m7s
docker / deploy-docs (push) Successful in 28s
deb / build-publish (push) Successful in 17m6s
ci / rust (push) Failing after 19m1s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m3s
flatpak / build-publish (push) Successful in 8m0s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m29s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m14s
release / apple (push) Successful in 5m44s
Wholesale commit of every uncommitted change across the tree, at the user's explicit request — host refactor-campaign W1 (native.rs facade + native/ dir, library/ + mgmt/ splits), Android, core. These streams were mid-flight and not individually built/tested together; this supersedes the per-session HOLD markers. Consolidating so everything lands on main in one pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
//! Lutris store provider: installed games from the Lutris SQLite DB + lutris.net CDN art. Split out of the `library` facade (plan §W5).
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Reads the **local** Lutris library DB (`pga.db`) — no network. Installed titles only; cover art
|
||||
/// from Lutris's on-disk cache, inlined as `data:` URLs. Linux-only (Lutris is Linux-only).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub struct LutrisProvider;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl LibraryProvider for LutrisProvider {
|
||||
fn store(&self) -> &'static str {
|
||||
"lutris"
|
||||
}
|
||||
|
||||
fn list(&self) -> Vec<GameEntry> {
|
||||
let Some(db) = lutris_db() else {
|
||||
return Vec::new();
|
||||
};
|
||||
lutris_games(&db).unwrap_or_else(|e| {
|
||||
tracing::warn!(error = %e, db = %db.display(), "lutris pga.db read failed — skipping");
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The first existing Lutris `pga.db`: XDG data dir, the classic `~/.local/share`, or Flatpak.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn lutris_db() -> Option<PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(d) = std::env::var_os("XDG_DATA_HOME") {
|
||||
candidates.push(PathBuf::from(d).join("lutris/pga.db"));
|
||||
}
|
||||
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
|
||||
candidates.push(home.join(".local/share/lutris/pga.db"));
|
||||
candidates.push(home.join(".var/app/net.lutris.Lutris/data/lutris/pga.db"));
|
||||
}
|
||||
candidates.into_iter().find(|p| p.is_file())
|
||||
}
|
||||
|
||||
/// Installed games from a Lutris `pga.db`. Opened **read-only + immutable** (via a SQLite URI) so a
|
||||
/// running Lutris holding the file can't make us block or fail, and we never write to it.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn lutris_games(db: &Path) -> rusqlite::Result<Vec<GameEntry>> {
|
||||
use rusqlite::OpenFlags;
|
||||
// `immutable=1` treats the DB as read-only-and-unchanging → no locking against a live Lutris. The
|
||||
// path goes into the URI literally; a `?`/`#` in it (vanishingly rare on Linux) would mis-parse,
|
||||
// so fall back to a plain read-only open in that case.
|
||||
let path = db.to_string_lossy();
|
||||
let conn = if path.contains('?') || path.contains('#') {
|
||||
rusqlite::Connection::open_with_flags(db, OpenFlags::SQLITE_OPEN_READ_ONLY)?
|
||||
} else {
|
||||
rusqlite::Connection::open_with_flags(
|
||||
format!("file:{path}?immutable=1"),
|
||||
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
|
||||
)?
|
||||
};
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, slug, name FROM games \
|
||||
WHERE installed = 1 AND name IS NOT NULL AND name <> '' \
|
||||
ORDER BY name COLLATE NOCASE",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, Option<String>>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
))
|
||||
})?;
|
||||
let mut games = Vec::new();
|
||||
for (id, slug, name) in rows.flatten() {
|
||||
games.push(GameEntry {
|
||||
id: format!("lutris:{id}"),
|
||||
store: "lutris".into(),
|
||||
title: name,
|
||||
art: slug.as_deref().map(lutris_art).unwrap_or_default(),
|
||||
launch: Some(LaunchSpec {
|
||||
kind: "lutris_id".into(),
|
||||
value: id.to_string(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
Ok(games)
|
||||
}
|
||||
|
||||
/// Lutris cover art (local files keyed by slug) inlined as `data:` URLs — Lutris has no public CDN
|
||||
/// keyed by a stable id (unlike Steam/Heroic), and `Artwork` fields are URLs the client fetches, so a
|
||||
/// self-contained `data:` URL needs no host-served endpoint. `coverart` → portrait, `banners` → header.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn lutris_art(slug: &str) -> Artwork {
|
||||
Artwork {
|
||||
portrait: lutris_image("coverart", slug),
|
||||
header: lutris_image("banners", slug),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Find `<kind>/<slug>.jpg` across the current (0.5.18+), legacy (`~/.cache`), and Flatpak Lutris
|
||||
/// dirs and inline it as `data:image/jpeg;base64,…`. Skips a missing or implausibly large file (a
|
||||
/// 1 MiB cap bounds the catalog JSON so a few big files can't bloat it).
|
||||
#[cfg(target_os = "linux")]
|
||||
fn lutris_image(kind: &str, slug: &str) -> Option<String> {
|
||||
use base64::Engine as _;
|
||||
let home = std::env::var_os("HOME").map(PathBuf::from)?;
|
||||
let roots = [
|
||||
home.join(".local/share/lutris"),
|
||||
home.join(".cache/lutris"),
|
||||
home.join(".var/app/net.lutris.Lutris/data/lutris"),
|
||||
home.join(".var/app/net.lutris.Lutris/cache/lutris"),
|
||||
];
|
||||
for root in roots {
|
||||
let p = root.join(kind).join(format!("{slug}.jpg"));
|
||||
let Ok(meta) = std::fs::metadata(&p) else {
|
||||
continue;
|
||||
};
|
||||
if meta.len() == 0 || meta.len() > 1024 * 1024 {
|
||||
continue;
|
||||
}
|
||||
if let Ok(bytes) = std::fs::read(&p) {
|
||||
let enc = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
return Some(format!("data:image/jpeg;base64,{enc}"));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn lutris_games_reads_installed_only() {
|
||||
use rusqlite::Connection;
|
||||
let dir = std::env::temp_dir().join(format!("pf-lutris-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let db = dir.join("pga.db");
|
||||
{
|
||||
let c = Connection::open(&db).unwrap();
|
||||
c.execute_batch(
|
||||
"CREATE TABLE games (id INTEGER PRIMARY KEY, slug TEXT, name TEXT, installed INTEGER);
|
||||
INSERT INTO games (id,slug,name,installed) VALUES (42,'elden-ring','ELDEN RING',1);
|
||||
INSERT INTO games (id,slug,name,installed) VALUES (7,'owned','Owned Only',0);
|
||||
INSERT INTO games (id,slug,name,installed) VALUES (9,'noname',NULL,1);",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let games = lutris_games(&db).unwrap();
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
// Only the installed, named row; the uninstalled + NULL-name rows are filtered out.
|
||||
assert_eq!(games.len(), 1);
|
||||
assert_eq!(games[0].id, "lutris:42");
|
||||
assert_eq!(games[0].store, "lutris");
|
||||
assert_eq!(games[0].title, "ELDEN RING");
|
||||
let l = games[0].launch.as_ref().unwrap();
|
||||
assert_eq!((l.kind.as_str(), l.value.as_str()), ("lutris_id", "42"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user