//! 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 index; pub mod tags; use serde::{Deserialize, Serialize}; use std::path::PathBuf; /// The filesystem roots the music indexer is confined to — one per source /// `13-MUSIC-MODEL.md` decided to index ("Sources indexed: both"): /// /// - `data_dir/filebrowser/Music` — the node's own FileBrowser `Music` /// folder (`MusicSource::OwnLibrary`). Peer purchases of audio are also /// auto-filed here by `content.*`'s paid-download path, so they enter the /// library through this root with their real filenames. /// - `data_dir/purchased-content` — the local byte cache of peer-purchased /// content (`MusicSource::Peer { onion }`), laid out as /// `/`. /// /// Confinement is a parameter everywhere downstream (`tags::extract_tags` /// and `index::reindex` both take these roots and refuse paths outside /// them) — an indexer that can be aimed at `data_dir/secrets` is a /// secret-exfiltration primitive (T-13-39), so the roots are computed in /// exactly one place and passed through. pub fn media_roots(config: &crate::config::Config) -> Vec { vec![ config.data_dir.join("filebrowser").join("Music"), config.data_dir.join("purchased-content"), ] } /// 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, 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, pub album: Option, pub album_artist: Option, pub track_number: Option, pub disc_number: Option, pub year: Option, 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, } /// 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, } /// 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, } /// A complete, consistent read of the library at one point in time: the /// persisted track rows plus the albums/artists derived from them /// (derived-albums, `13-MUSIC-MODEL.md`). Produced by /// `index::MusicIndex::snapshot`; because the on-disk index is written /// atomically (`index::save_atomic`), a snapshot is always taken from a /// complete index — never a partially-written one. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct LibrarySnapshot { pub schema_version: u32, /// RFC3339 timestamp of the scan this snapshot was read from. Populated /// even for an empty, never-scanned library (with the time the empty /// snapshot was produced) — an empty library is empty arrays plus a /// timestamp, never a null and never an error. pub scanned_at: String, pub tracks: Vec, pub albums: Vec, pub artists: Vec, }