feat(13-07): persisted music index — scan, group, atomic save, incremental refresh
- music/index.rs: reindex + refresh_incremental over the media roots, (path, mtime, size) diffing so unchanged files never re-extract, rows removed when files disappear (derived albums vanish with their last track), per-file extraction errors counted in ScanStats.skipped - save_atomic: temp sibling + fsync + rename — a concurrent read sees a complete index or the previous one, never a partial file (T-13-42) - load refuses schema_version > MUSIC_SCHEMA_VERSION with a distinct NewerSchema error and never overwrites the newer file (T-13-43) - symlinks whose canonical target escapes the media roots are skipped, not followed (T-13-39); confinement enforced here and in tags.rs - reindex guard: AtomicBool + RAII release; a second concurrent scan reports already-running instead of duplicating the walk (T-13-41) - music/mod.rs: media_roots(Config) (filebrowser/Music + purchased-content) and LibrarySnapshot (tracks + derived albums/artists) - 9 tests, one per 13-07 Task 1 behavior bullet, programmatic FLAC fixtures into tempdirs (no committed binaries) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
589cbb030f
commit
49687f7ebd
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,35 @@
|
||||
//! `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
|
||||
/// `<onion>/<content_id>`.
|
||||
///
|
||||
/// 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<PathBuf> {
|
||||
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
|
||||
@@ -103,3 +127,22 @@ pub struct Artist {
|
||||
pub id: ArtistId,
|
||||
pub track_ids: Vec<TrackId>,
|
||||
}
|
||||
|
||||
/// 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<Track>,
|
||||
pub albums: Vec<Album>,
|
||||
pub artists: Vec<Artist>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user