Files
archy/core/archipelago/src/music/mod.rs
T
archipelagoandClaude Fable 5 be8f24b4e3 wip(13-04): checkpoint Task 3 tag-extraction work recovered after broken pipe
Verbatim checkpoint of uncommitted executor work (music/mod.rs, music/tags.rs,
mod music; in main.rs) before verification. Tests not yet run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:05:12 -04:00

106 lines
4.4 KiB
Rust

//! Music domain root — entity types decided in `13-MUSIC-MODEL.md` (D-13,
//! one-way). See that document for the full rationale and the rejected
//! alternatives; this module implements the decision, it does not re-derive
//! it.
//!
//! Track identity is hybrid: `(source, canonical path)` is the row key
//! (cheap, stat-only, incremental via mtime), with a lazily-backfilled
//! content-hash dedupe column (`Track::content_hash`) for the move/dedupe
//! case that identity alone can't handle. Albums and artists are derived at
//! read time by grouping tracks on their tags, not stored as first-class
//! rows. The on-disk index is a single JSON file at
//! `data_dir/music/index.json`, matching `content_server.rs::load_catalog`'s
//! precedent.
pub mod tags;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Schema version of the on-disk music index (`data_dir/music/index.json`).
/// Bump when `Track`'s shape changes in a way that needs a reindex. See
/// `13-MUSIC-MODEL.md`'s "Schema version and the reindex path" section for
/// the newer-version-on-older-binary handling contract: an older binary
/// encountering a newer-versioned index treats it as absent rather than
/// reinterpreting or overwriting it.
pub const MUSIC_SCHEMA_VERSION: u32 = 1;
/// Where a track was discovered. Both sources are indexed
/// (`13-MUSIC-MODEL.md`'s "Sources indexed: both").
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MusicSource {
/// The node's own FileBrowser `Music` folder.
OwnLibrary,
/// A peer's shared audio, reachable via the content/peer-proxy
/// subsystem.
Peer { onion: String },
}
/// Stable identity for a track (hybrid-identity, `13-MUSIC-MODEL.md`):
/// `(source, canonical path)` is the row key — cheap, stat-only, survives a
/// rescan via mtime. A file move or rename orphans this identity;
/// `Track::content_hash` is the lazily-backfilled dedupe column that exists
/// for exactly that case, and for cross-peer dedupe once it's populated.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TrackId {
pub source: MusicSource,
pub path: PathBuf,
}
/// Derived album grouping key (derived-albums, `13-MUSIC-MODEL.md`). Albums
/// are not stored rows — this is the key produced by grouping `Track`s at
/// read time on `(album_artist, album)`, not a persisted identity. A retag
/// simply changes what the grouping produces on the next read.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AlbumId {
pub album_artist: Option<String>,
pub album: String,
}
/// Derived artist grouping key. Same read-time-only status as `AlbumId`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ArtistId(pub String);
/// A single indexed track — a row in `data_dir/music/index.json`. This is
/// the migration surface `13-MUSIC-MODEL.md`'s one-way decision is about:
/// changing this shape after nodes have indexed libraries needs a reindex
/// path (`MUSIC_SCHEMA_VERSION`), not just a code change.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Track {
pub id: TrackId,
pub title: String,
pub artist: Option<String>,
pub album: Option<String>,
pub album_artist: Option<String>,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub year: Option<u32>,
pub duration_secs: u64,
/// `false` when `title` was derived from the filename stem because the
/// file carried no readable tags — the track still appears in the
/// library rather than being dropped (see `tags::extract_tags`).
pub has_tags: bool,
/// Lazily-backfilled dedupe column (hybrid-identity,
/// `13-MUSIC-MODEL.md`). `None` until a background backfill pass
/// computes it; absence is not an error state.
#[serde(default)]
pub content_hash: Option<String>,
}
/// An album, computed at read time by grouping `Track`s on
/// `(album_artist, album)` — never persisted directly (derived-albums,
/// `13-MUSIC-MODEL.md`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Album {
pub id: AlbumId,
pub track_ids: Vec<TrackId>,
}
/// An artist, computed at read time by grouping `Track`s on `artist`. Same
/// read-time-only status as `Album`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Artist {
pub id: ArtistId,
pub track_ids: Vec<TrackId>,
}