Add mode READMEs and move Bigscreen app
Add comprehensive READMEs for Bigscreen and Desktop modes, and relocate the Tauri frontend prototype into a new Bigscreen/ subdirectory. This moves package.json/package-lock, src, src-tauri, assets, views, styles and related files into Bigscreen/ to separate the controller-first shell from top-level docs. Update the root README.md to reframe the project as "NebulaOS", describe Bigscreen/Desktop modes, the vision, tech stack and development notes. This reorganizes the repo layout for clearer mode separation and documentation.
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
use super::images::ImageCache;
|
||||
use super::models::{
|
||||
infer_library_item_kind, GameCandidate, GameMetadata, GameSource, MetadataStatus,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
pub mod providers {
|
||||
pub mod igdb {
|
||||
use crate::library::metadata::MetadataProvider;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait IgdbMetadataProvider: MetadataProvider {}
|
||||
}
|
||||
|
||||
pub mod rawg {
|
||||
use crate::library::metadata::MetadataProvider;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait RawgMetadataProvider: MetadataProvider {}
|
||||
}
|
||||
|
||||
pub mod steam {
|
||||
use crate::library::metadata::MetadataProvider;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait SteamMetadataProvider: MetadataProvider {}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MetadataProvider: Send + Sync {
|
||||
#[allow(dead_code)]
|
||||
fn provider_id(&self) -> &'static str;
|
||||
fn resolve(&self, candidate: &GameCandidate) -> Result<Option<GameMetadata>, String>;
|
||||
}
|
||||
|
||||
pub struct LocalCacheMetadataProvider {
|
||||
image_cache: ImageCache,
|
||||
}
|
||||
|
||||
impl LocalCacheMetadataProvider {
|
||||
pub fn new(image_cache: ImageCache) -> Self {
|
||||
Self { image_cache }
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataProvider for LocalCacheMetadataProvider {
|
||||
fn provider_id(&self) -> &'static str {
|
||||
"local_cache"
|
||||
}
|
||||
|
||||
fn resolve(&self, candidate: &GameCandidate) -> Result<Option<GameMetadata>, String> {
|
||||
if candidate.source == GameSource::Steam {
|
||||
return self.resolve_steam(candidate);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalCacheMetadataProvider {
|
||||
fn resolve_steam(&self, candidate: &GameCandidate) -> Result<Option<GameMetadata>, String> {
|
||||
let Some(app_id) = candidate.app_id.as_deref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let store_data = fetch_steam_store_data(app_id)?;
|
||||
let title = store_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("name"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| Some(candidate.title.clone()));
|
||||
let description = store_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("short_description"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
let genres = store_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("genres"))
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.get("description").and_then(Value::as_str))
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let steam_categories = store_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("categories"))
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.get("description").and_then(Value::as_str))
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let steam_app_type = store_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("type"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
let app_kind = infer_library_item_kind(
|
||||
steam_app_type.as_deref(),
|
||||
title.as_deref(),
|
||||
&genres,
|
||||
&steam_categories,
|
||||
);
|
||||
let release_date = store_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("release_date"))
|
||||
.and_then(|value| value.get("date"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
let developer = joined_string_array(store_data.as_ref(), "developers");
|
||||
let publisher = joined_string_array(store_data.as_ref(), "publishers");
|
||||
let header_image = store_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("header_image"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
|
||||
let source = candidate.source.as_str();
|
||||
let cover_image = self.image_cache.cache_first_remote_image(
|
||||
source,
|
||||
app_id,
|
||||
"cover",
|
||||
"jpg",
|
||||
&[
|
||||
format!("https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/library_600x900.jpg"),
|
||||
format!("https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/{app_id}/library_600x900.jpg"),
|
||||
format!("https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/capsule_616x353.jpg"),
|
||||
],
|
||||
)?;
|
||||
let hero_urls = [
|
||||
Some(format!(
|
||||
"https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/library_hero.jpg"
|
||||
)),
|
||||
header_image.clone(),
|
||||
Some(format!(
|
||||
"https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/header.jpg"
|
||||
)),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
let hero_image = self
|
||||
.image_cache
|
||||
.cache_first_remote_image(source, app_id, "hero", "jpg", &hero_urls)?;
|
||||
let icon_image = self.image_cache.cache_first_remote_image(
|
||||
source,
|
||||
app_id,
|
||||
"icon",
|
||||
"jpg",
|
||||
&[
|
||||
format!(
|
||||
"https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/capsule_184x69.jpg"
|
||||
),
|
||||
format!("https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/header.jpg"),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(Some(GameMetadata {
|
||||
title,
|
||||
description,
|
||||
app_kind,
|
||||
steam_app_type,
|
||||
genres,
|
||||
steam_categories,
|
||||
release_date,
|
||||
developer,
|
||||
publisher,
|
||||
cover_image,
|
||||
hero_image,
|
||||
icon_image,
|
||||
status: MetadataStatus::Matched,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_steam_store_data(app_id: &str) -> Result<Option<Value>, String> {
|
||||
let url = format!("https://store.steampowered.com/api/appdetails?appids={app_id}");
|
||||
let response = match ureq::get(&url)
|
||||
.header("User-Agent", "NebulaOS/0.1 library-scanner")
|
||||
.call()
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut response = response;
|
||||
let body = response
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.map_err(|err| format!("Failed to read Steam metadata response: {err}"))?;
|
||||
let value: Value = serde_json::from_str(&body)
|
||||
.map_err(|err| format!("Failed to parse Steam metadata: {err}"))?;
|
||||
|
||||
Ok(value
|
||||
.get(app_id)
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.get("success")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.and_then(|entry| entry.get("data"))
|
||||
.cloned())
|
||||
}
|
||||
|
||||
fn joined_string_array(value: Option<&Value>, key: &str) -> Option<String> {
|
||||
value
|
||||
.and_then(|value| value.get(key))
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
})
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct MetadataMatchPlan {
|
||||
pub exact_app_id: bool,
|
||||
pub store_specific_lookup: bool,
|
||||
pub fuzzy_title_matching: bool,
|
||||
pub manual_user_correction: bool,
|
||||
}
|
||||
|
||||
impl Default for MetadataMatchPlan {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
exact_app_id: true,
|
||||
store_specific_lookup: true,
|
||||
fuzzy_title_matching: true,
|
||||
manual_user_correction: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user