diff --git a/src-tauri/crates/app/src/commands/browse.rs b/src-tauri/crates/app/src/commands/browse.rs index 38701d7a..903723dc 100644 --- a/src-tauri/crates/app/src/commands/browse.rs +++ b/src-tauri/crates/app/src/commands/browse.rs @@ -1262,7 +1262,17 @@ struct ArtistAlbumRawRow { artwork_format: Option, } -/// Return full artist detail: header, discography, and track count. +/// Loads an artist's metadata, artwork, available-track counts, and albums. +/// +/// # Examples +/// +/// ```no_run +/// let detail = get_artist_detail(state, artist_id).await?; +/// assert_eq!(detail.id, artist_id); +/// # Ok::<(), crate::error::AppError>(()) +/// ``` +/// +/// Returns an error when the artist does not exist or the profile data cannot be loaded. #[tauri::command] pub async fn get_artist_detail( state: tauri::State<'_, AppState>, diff --git a/src-tauri/crates/app/src/commands/deezer.rs b/src-tauri/crates/app/src/commands/deezer.rs index bc16c049..41b1b49e 100644 --- a/src-tauri/crates/app/src/commands/deezer.rs +++ b/src-tauri/crates/app/src/commands/deezer.rs @@ -273,6 +273,16 @@ pub struct DeezerArtistEnrichment { } impl DeezerArtistEnrichment { + /// Creates an artist enrichment value with no Deezer metadata or artwork. + /// + /// # Examples + /// + /// ``` + /// let enrichment = DeezerArtistEnrichment::empty(); + /// assert!(enrichment.deezer_id.is_none()); + /// assert!(enrichment.picture_url.is_none()); + /// assert!(enrichment.background_path.is_none()); + /// ``` fn empty() -> Self { Self { deezer_id: None, @@ -340,6 +350,18 @@ async fn enrich_artist_deezer_with_pool( Ok(enrichment) } +/// Enriches a local artist with cached or remotely fetched Deezer, biography, and background artwork metadata. +/// +/// Uses the configured biography provider and language, respects offline mode, and persists newly +/// resolved metadata for subsequent requests. Returns an empty enrichment when the artist or a +/// remote match is unavailable. +/// +/// # Examples +/// +/// ```no_run +/// let enrichment = enrich_artist_deezer_inner(state, pool, artist_id).await?; +/// # Ok::<(), AppError>(()) +/// ``` async fn enrich_artist_deezer_inner( state: tauri::State<'_, AppState>, pool: sqlx::SqlitePool, diff --git a/src-tauri/crates/core/src/artwork/metadata.rs b/src-tauri/crates/core/src/artwork/metadata.rs index d60350a8..800dd06e 100644 --- a/src-tauri/crates/core/src/artwork/metadata.rs +++ b/src-tauri/crates/core/src/artwork/metadata.rs @@ -38,16 +38,35 @@ pub fn existing_path(dir: &Path, hash: &str) -> Option { } } -/// Resolve an artist/album picture path preferring a local profile-artwork -/// sidecar (`/.`, e.g. `artist.jpg` -/// imported into the profile) over the shared Deezer metadata cache -/// ([`existing_path`]). Each candidate is returned only when the file -/// actually exists on disk, so a stale DB reference to a wiped file falls -/// through to the next source instead of yielding a broken path (#350). -/// -/// The local sidecar carries its own `format` column (jpg/png/webp) so we -/// take it explicitly; the metadata cache is always `.jpg`, so `cache_hash` -/// goes through [`existing_path`] which knows the extension. +/// Resolves an artwork path, preferring an existing local sidecar over the shared cache. +/// +/// Local artwork uses the supplied hash and format to form `/.`. +/// If that file is unavailable, the function checks the shared cache. Missing or stale +/// references yield `None`. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// +/// let path = resolve_local_or_cached_path( +/// Path::new("/tmp/profile"), +/// None, +/// None, +/// Path::new("/tmp/cache"), +/// None, +/// ); +/// +/// assert_eq!(path, None); +/// ``` +/// +/// # Parameters +/// +/// * `local_format` - The file extension of the local sidecar, such as `jpg`, `png`, or `webp`. +/// +/// # Returns +/// +/// The path to the first existing artwork file, or `None` when no referenced file exists. pub fn resolve_local_or_cached_path( local_dir: &Path, local_hash: Option<&str>, @@ -64,26 +83,67 @@ pub fn resolve_local_or_cached_path( cache_hash.and_then(|h| existing_path(cache_dir, h)) } -/// Download `url`, blake3-hash the bytes and write the file to -/// `/.jpg` if missing, then queue the `_1x` / `_2x` thumbnail -/// job. Returns the hex hash on success. +/// Downloads an image, caches it under its BLAKE3 hash, and queues thumbnail generation. +/// +/// The cached file is written as `/.jpg` when it does not already exist. +/// Returns the hexadecimal hash when the download and caching succeed; failures are +/// logged and produce `None`. /// -/// All failures (network, http != 2xx, oversize body, write error) are logged -/// at WARN level and surfaced as `None`. Enrichment is best-effort: the -/// caller should fall back to the remote URL. +/// # Examples +/// +/// ```no_run +/// # async fn example() { +/// let hash = download_and_cache("https://example.com/artwork.jpg", std::path::Path::new("/tmp/artwork")).await; +/// assert!(hash.is_some()); +/// # } +/// ``` +/// +/// # Returns +/// +/// The hexadecimal BLAKE3 hash of the cached image, or `None` if downloading, +/// reading, validation, or writing fails. +pub async fn download_and_cache(url: &str, dir: &Path) -> Option pub async fn download_and_cache(url: &str, dir: &Path) -> Option { download_and_cache_inner(url, dir, true).await } -/// Same as [`download_and_cache`] but **skips thumbnail generation**. +/// Downloads an image and caches it without generating thumbnails. +/// +/// # Returns +/// +/// The BLAKE3 hash of the cached image, or `None` if the download or caching fails. /// -/// For images only ever consumed at full size — the artist hero fanart -/// (#482) is painted full-bleed, so a downscaled tier would only soften -/// it — the `_1x` / `_2x` job is pure CPU + disk for files nothing reads. +/// # Examples +/// +/// ```no_run +/// # async fn example() { +/// let hash = download_and_cache_full_res( +/// "https://example.com/artist-fanart.jpg", +/// std::path::Path::new("/tmp/artwork"), +/// ).await; +/// # } +/// ``` pub async fn download_and_cache_full_res(url: &str, dir: &Path) -> Option { download_and_cache_inner(url, dir, false).await } +/// Downloads artwork, caches it under its content hash, and optionally queues thumbnail generation. +/// +/// Returns `None` when the download fails, the response is unsuccessful, the response body is empty or too large, or the cached file cannot be written. +/// +/// # Examples +/// +/// ```no_run +/// # async fn example() { +/// let hash = download_and_cache_inner( +/// "https://example.com/artwork.jpg", +/// std::path::Path::new("/tmp/artwork"), +/// true, +/// ) +/// .await; +/// assert!(hash.is_some()); +/// # } +/// ``` async fn download_and_cache_inner( url: &str, dir: &Path, diff --git a/src-tauri/crates/core/src/metadata/theaudiodb.rs b/src-tauri/crates/core/src/metadata/theaudiodb.rs index 5abae791..db8ab201 100644 --- a/src-tauri/crates/core/src/metadata/theaudiodb.rs +++ b/src-tauri/crates/core/src/metadata/theaudiodb.rs @@ -89,9 +89,20 @@ struct ArtistPayload { } impl ArtistPayload { - /// Pick the biography for `lang`, falling back to English when the - /// requested language is missing or blank. `lang` is a short code - /// (`"fr"`, `"de"`, … `"zh"`); anything unmapped resolves to English. + /// Selects the biography for a supported language and falls back to English when the selected biography is unavailable or blank. + /// + /// Supported language codes are `fr`, `de`, `es`, `it`, `pt`, `nl`, `ru`, `ja`, and `zh`; other codes select English. + /// + /// # Examples + /// + /// ``` + /// let payload = ArtistPayload { + /// bio_en: Some("English biography".into()), + /// ..Default::default() + /// }; + /// + /// assert_eq!(payload.bio_for_lang("fr"), Some("English biography".into())); + /// ``` fn bio_for_lang(&self, lang: &str) -> Option { let primary = match lang { "fr" => &self.bio_fr, @@ -108,9 +119,24 @@ impl ArtistPayload { non_blank(primary).or_else(|| non_blank(&self.bio_en)) } - /// First non-blank wide image, widest-and-cleanest first: real - /// fanart, then its community alternates, then the wide thumb, and - /// the logo banner only as a last resort. + /// Selects the first available wide artist image in priority order. + /// + /// Blank image URLs are skipped. Fanart fields take precedence over the wide thumbnail, + /// which takes precedence over the banner. + /// + /// # Examples + /// + /// ``` + /// let payload = ArtistPayload { + /// fanart: Some("https://example.com/fanart.jpg".into()), + /// ..Default::default() + /// }; + /// + /// assert_eq!( + /// payload.fanart_url(), + /// Some("https://example.com/fanart.jpg".into()) + /// ); + /// ``` fn fanart_url(&self) -> Option { non_blank(&self.fanart) .or_else(|| non_blank(&self.fanart2)) @@ -146,6 +172,13 @@ impl Default for TheAudioDbClient { } impl TheAudioDbClient { + /// Creates a client for communicating with TheAudioDB. + /// + /// # Examples + /// + /// ``` + /// let _client = TheAudioDbClient::new(); + /// ``` pub fn new() -> Self { let http = reqwest::Client::builder() .user_agent(USER_AGENT) @@ -155,12 +188,27 @@ impl TheAudioDbClient { Self { http } } - /// Look up an artist by name, returning its bio in `lang` (English - /// fallback) and its wide fanart URL. Returns `Ok(None)` only when - /// nothing matches the name — a match with neither bio nor fanart - /// still comes back as `Some` with both fields empty, so the caller - /// can cache the "looked it up, nothing there" outcome instead of - /// re-querying a rate-limited API on every visit. + /// Looks up an artist by name and returns localized biography and fanart information. + /// + /// The biography uses the requested language with an English fallback. A matching + /// artist is returned even when no biography or fanart is available; `None` means + /// that no artist matched the requested name. + /// + /// # Examples + /// + /// ```no_run + /// async fn lookup_artist( + /// client: &TheAudioDbClient, + /// ) -> reqwest::Result<()> { + /// let artist = client.artist_info("Daft Punk", "en").await?; + /// + /// if let Some(artist) = artist { + /// println!("{}", artist.name); + /// } + /// + /// Ok(()) + /// } + /// ``` pub async fn artist_info( &self, name: &str, diff --git a/src/components/common/ArtistHeroBackdrop.tsx b/src/components/common/ArtistHeroBackdrop.tsx index fdf53f55..c9579ead 100644 --- a/src/components/common/ArtistHeroBackdrop.tsx +++ b/src/components/common/ArtistHeroBackdrop.tsx @@ -21,11 +21,10 @@ * hero dissolves into whatever the current theme/skin paints behind it. */ /** - * Escape a URL for interpolation inside a double-quoted CSS `url("…")`. - * Unlike the local artwork paths other backdrops paint, a hero source can - * be a **remote URL straight out of TheAudioDB** — third-party data that - * must not be able to close the string and inject CSS. Inside a quoted - * string only the backslash, the closing quote and raw newlines matter. + * Escapes a source for use inside a double-quoted CSS `url(...)` value. + * + * @param src - The source to escape + * @returns The source with backslashes and double quotes escaped and raw line breaks removed */ function cssUrl(src: string): string { return src.replace(/[\\"]/g, "\\$&").replace(/[\n\r\f]/g, ""); @@ -38,6 +37,13 @@ interface ArtistHeroBackdropProps { isFanart: boolean; } +/** + * Renders a decorative artist backdrop with styling based on the image type. + * + * @param src - The image source used for the backdrop + * @param isFanart - Whether the source is fanart rather than a square artist photo + * @returns The backdrop element, or `null` when no source is provided + */ export function ArtistHeroBackdrop({ src, isFanart }: ArtistHeroBackdropProps) { if (!src) return null; diff --git a/src/components/views/ArtistDetailView.tsx b/src/components/views/ArtistDetailView.tsx index 18506942..819f39d3 100644 --- a/src/components/views/ArtistDetailView.tsx +++ b/src/components/views/ArtistDetailView.tsx @@ -50,6 +50,13 @@ interface ArtistDetailViewProps { onNavigateToArtist: (artistId: number) => void; } +/** + * Renders an artist detail page with artwork, metadata, related artists, albums, and tracks. + * + * @param artistId - The library artist identifier to display + * @param onNavigateToAlbum - Handles navigation to an album + * @param onNavigateToArtist - Handles navigation to a library artist + */ export function ArtistDetailView({ artistId, onNavigateToAlbum, diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index 965fe9cb..fec7b05e 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -522,6 +522,12 @@ function LanguageDropdown({ currentCode, onSelect }: LanguageDropdownProps) { ); } +/** + * Renders the categorized application settings interface. + * + * @param onNavigate - Callback used to navigate to another application view. + * @returns The settings view. + */ export function SettingsView({ onNavigate }: SettingsViewProps) { const { t, i18n } = useTranslation(); const { theme, setThemeId } = useTheme(); diff --git a/src/components/views/settings/ArtistHeroCard.tsx b/src/components/views/settings/ArtistHeroCard.tsx index e34a5131..9a05a5a7 100644 --- a/src/components/views/settings/ArtistHeroCard.tsx +++ b/src/components/views/settings/ArtistHeroCard.tsx @@ -3,9 +3,9 @@ import { PanelTop } from "lucide-react"; import { useArtistHero } from "../../../hooks/useArtistHero"; /** - * Settings → Appearance row toggling the full-bleed hero backdrop behind - * the artist detail header (issue #482). Default ON — turning it off - * restores the flat header with just the circular photo. + * Renders an appearance setting for enabling or disabling the artist detail hero backdrop. + * + * @returns A settings row containing a controlled checkbox for the hero backdrop. */ export function ArtistHeroCard() { const { t } = useTranslation(); diff --git a/src/hooks/useArtistHero.ts b/src/hooks/useArtistHero.ts index cc72c441..f5e5a052 100644 --- a/src/hooks/useArtistHero.ts +++ b/src/hooks/useArtistHero.ts @@ -13,6 +13,12 @@ export const ARTIST_HERO_EVENT = "waveflow:artist-hero"; * toggle is there for users who prefer the flat header. */ const DEFAULT_ENABLED = true; +/** + * Parses a stored preference value into an enabled state. + * + * @param raw - The stored preference value + * @returns `true` for `"true"` or `"1"`, the default enabled state for a missing value, and `false` for other values + */ function parseEnabled(raw: string | null): boolean { if (raw == null) return DEFAULT_ENABLED; return raw === "true" || raw === "1"; @@ -32,12 +38,14 @@ export interface ArtistHero { } /** - * Per-profile preference: paint a full-bleed hero backdrop behind the - * artist detail header (issue #482) — the wide TheAudioDB fanart when the - * artist has one, a blurred version of the square photo otherwise. Default - * ON. The write machinery mirrors [`useCoverSlideshow`](./useCoverSlideshow.ts) - * — serialized writes, profile-switch guards, and rollback to the last - * backend-confirmed value. + * Manages the per-profile artist hero backdrop preference. + * + * The preference is enabled by default and synchronized with the active + * profile. Updates are applied optimistically and rolled back to the last + * confirmed value if the latest write fails. + * + * @returns The current enabled state, whether the active profile's preference + * has been resolved, and a function for updating the preference. */ export function useArtistHero(): ArtistHero { const { activeProfile } = useProfile(); diff --git a/src/lib/tauri/detail.ts b/src/lib/tauri/detail.ts index e06d83e6..024aeeea 100644 --- a/src/lib/tauri/detail.ts +++ b/src/lib/tauri/detail.ts @@ -141,6 +141,12 @@ export interface DeezerArtistEnrichment { background_path: string | null; } +/** + * Enriches an album with metadata from Deezer. + * + * @param albumId - The identifier of the album to enrich + * @returns Deezer metadata for the album + */ export function enrichAlbumDeezer( albumId: number, ): Promise {