Persist provider_item_id through fsdy link tomls

A link/queue/bookmark toml now carries the source track's provider id, so
a later capture of a linked track can de-duplicate by provider id before
downloading instead of falling back to hashing fetched bytes. Empty when
the source has none (e.g. a local /fs file). The scan CLI fills the new
field explicitly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-22 22:35:39 +02:00
parent b50cf862b3
commit 6ca8607586
2 changed files with 45 additions and 3 deletions

View File

@ -235,6 +235,8 @@ async fn scan_file(
artist: String::new(), artist: String::new(),
duration: None, duration: None,
album: None, album: None,
// A scanned local file has no provider-internal id.
provider_item_id: None,
playable, playable,
}; };
tokio::fs::write(&sidecar, track_file.to_toml()?).await?; tokio::fs::write(&sidecar, track_file.to_toml()?).await?;

View File

@ -81,6 +81,14 @@ pub struct TrackFile {
/// Optional album metadata. /// Optional album metadata.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub album: Option<AlbumMeta>, pub album: Option<AlbumMeta>,
/// Provider-internal id of the source item (`Track.provider_item_id`),
/// preserved so it survives a link/queue/bookmark round trip. Without it
/// a later capture of a linked track cannot de-duplicate by provider id
/// *before* downloading — it falls back to hashing the fetched bytes
/// (architecture/crabidy-store.md D3/D4). Omitted when the source has
/// none (e.g. a local `/fs` file).
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_item_id: Option<String>,
/// The playable reference; exactly one of its fields must be set. /// The playable reference; exactly one of its fields must be set.
pub playable: PlayableSpec, pub playable: PlayableSpec,
} }
@ -218,9 +226,10 @@ impl TrackFile {
release_date: a.release_date.clone(), release_date: a.release_date.clone(),
}), }),
is_skipped: matches!(playable, Ok(Playable::Skipped)), is_skipped: matches!(playable, Ok(Playable::Skipped)),
// The provider that owns a link target sets this at its end; a // Restored from the toml when present (a link/queue/bookmark keeps
// local file/store/skipped track has no provider-internal id. // the source's provider id so a later capture can de-dup by id);
provider_item_id: String::new(), // empty when the source had none.
provider_item_id: self.provider_item_id.clone().unwrap_or_default(),
// A store-backed track is captured by construction; other // A store-backed track is captured by construction; other
// playables are marked (or not) by the server's store index. // playables are marked (or not) by the server's store index.
is_captured: matches!(playable, Ok(Playable::Store(_))), is_captured: matches!(playable, Ok(Playable::Store(_))),
@ -264,6 +273,10 @@ impl TrackFile {
title: a.title.clone(), title: a.title.clone(),
release_date: a.release_date.clone(), release_date: a.release_date.clone(),
}), }),
// Keep the source's provider id so a capture of this saved entry
// can de-dup by id before downloading (crabidy-store.md D4).
provider_item_id: (!track.provider_item_id.is_empty())
.then(|| track.provider_item_id.clone()),
playable, playable,
} }
} }
@ -1503,6 +1516,33 @@ mod tests {
assert_eq!(restored, track); assert_eq!(restored, track);
} }
#[test]
fn link_files_preserve_the_provider_item_id() {
// A streamed track (e.g. Tidal) carries a provider id; a link save
// (queue/bookmark) must keep it so a later capture of that save can
// de-duplicate by id before downloading (crabidy-store.md D4).
let track = Track {
path: "/tidal/albums/1/125169484".to_string(),
artist: "Efence".to_string(),
title: "Bay of Lost Dreams".to_string(),
duration: Some(216),
album: None,
is_skipped: false,
provider_item_id: "125169484".to_string(),
is_captured: false,
};
let toml_text = TrackFile::from_track(&track).to_toml().expect("serialize");
assert!(
toml_text.contains("provider_item_id = \"125169484\""),
"the id is written to the link toml: {toml_text}"
);
let restored = TrackFile::parse(&toml_text)
.expect("reparse")
.to_track("/crabidy/current/0001 Bay of Lost Dreams.cbd-track.toml");
assert_eq!(restored.provider_item_id, "125169484");
assert_eq!(restored, track);
}
#[tokio::test] #[tokio::test]
async fn from_track_with_file_plays_the_relative_sibling() { async fn from_track_with_file_plays_the_relative_sibling() {
let track = Track { let track = Track {