1use super::*;
4use crate::json::Json;
5use crate::model::AudioFile;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct TrackIdentity {
14 pub path: String,
16 pub size: u64,
18 pub codec: String,
20 pub duration_ms: Option<u64>,
22 pub sample_rate: Option<u32>,
24 pub bit_depth: Option<u16>,
26 pub channels: Option<u16>,
28 pub metadata: BTreeMap<String, Vec<String>>,
30 pub fingerprint: Option<String>,
32}
33
34impl TrackIdentity {
35 fn of(file: &AudioFile) -> Self {
36 let metadata = [
37 "title",
38 "artist",
39 "artists",
40 "albumartist",
41 "albumartists",
42 "album",
43 "tracknumber",
44 "discnumber",
45 "musicbrainz_recordingid",
46 "musicbrainz_albumid",
47 "isrc",
48 ]
49 .into_iter()
50 .filter_map(|key| {
51 file.tags
52 .get(key)
53 .map(|values| (key.into(), values.clone()))
54 })
55 .collect();
56 Self {
57 path: file.path.clone(),
58 size: file.size,
59 codec: file.properties.codec.clone(),
60 duration_ms: file.properties.duration_ms,
61 sample_rate: file.properties.sample_rate,
62 bit_depth: file.properties.bit_depth,
63 channels: file.properties.channels,
64 metadata,
65 fingerprint: file.fingerprint.as_ref().map(|fp| fp.data.clone()),
66 }
67 }
68
69 fn matches(&self, file: &AudioFile) -> bool {
70 let current = Self::of(file);
71 self.size == current.size
72 && self.codec == current.codec
73 && self.duration_ms == current.duration_ms
74 && self.sample_rate == current.sample_rate
75 && self.bit_depth == current.bit_depth
76 && self.channels == current.channels
77 && self.metadata == current.metadata
78 && self
79 .fingerprint
80 .as_ref()
81 .is_none_or(|fp| current.fingerprint.as_ref() == Some(fp))
82 }
83
84 pub(super) fn to_json(&self) -> Json {
85 let mut row = Json::obj();
86 row.set("path", self.path.as_str().into());
87 row.set("size", self.size.into());
88 row.set("codec", self.codec.as_str().into());
89 if let Some(value) = self.duration_ms {
90 row.set("duration_ms", value.into());
91 }
92 if let Some(value) = self.sample_rate {
93 row.set("sample_rate", value.into());
94 }
95 if let Some(value) = self.bit_depth {
96 row.set("bit_depth", u32::from(value).into());
97 }
98 if let Some(value) = self.channels {
99 row.set("channels", u32::from(value).into());
100 }
101 if let Some(value) = &self.fingerprint {
102 row.set("fingerprint", value.as_str().into());
103 }
104 let mut metadata = Json::obj();
105 for (key, values) in &self.metadata {
106 metadata.set(
107 key,
108 Json::Arr(values.iter().map(|value| value.as_str().into()).collect()),
109 );
110 }
111 row.set("metadata", metadata);
112 row
113 }
114
115 pub(super) fn from_json(row: &Json) -> Option<Self> {
116 let Json::Obj(fields) = row.get("metadata")? else {
117 return None;
118 };
119 let metadata = fields
120 .iter()
121 .map(|(key, values)| {
122 Some((
123 key.clone(),
124 values
125 .as_arr()?
126 .iter()
127 .map(Json::as_string)
128 .collect::<Option<Vec<_>>>()?,
129 ))
130 })
131 .collect::<Option<BTreeMap<_, _>>>()?;
132 Some(Self {
133 path: row.field_str("path")?,
134 size: row.field_u64("size")?,
135 codec: row.field_str("codec")?,
136 duration_ms: row.field_u64("duration_ms"),
137 sample_rate: row.field_u32("sample_rate"),
138 bit_depth: row.field_u32("bit_depth").and_then(|n| n.try_into().ok()),
139 channels: row.field_u32("channels").and_then(|n| n.try_into().ok()),
140 metadata,
141 fingerprint: row.field_str("fingerprint"),
142 })
143 }
144}
145
146#[derive(Debug, Clone, Copy, Default, PartialEq)]
148pub struct Attachment {
149 pub attached: usize,
151 pub waiting: usize,
153 pub moved: usize,
155}
156
157pub(super) fn resolve_release(reference: &EntityRef, catalog: &Catalog) -> Option<Id> {
158 if let Some(release) = catalog.releases.iter().find(|release| {
159 EntityRef::of(catalog, EntityKind::Release, release.id).as_ref() == Some(reference)
160 }) {
161 return Some(release.id);
162 }
163 let (prefix, folder) = reference.key.rsplit_once(RELEASE_KEY_SEPARATOR)?;
164 let legacy_parent = text::disc_folder(text::file_name(folder)).map(|_| text::folder(folder));
165 let mut matches = catalog.releases.iter().filter(|release| {
166 EntityRef::of(catalog, EntityKind::Release, release.id).is_some_and(|current| {
167 let Some((current_prefix, current_folder)) =
168 current.key.rsplit_once(RELEASE_KEY_SEPARATOR)
169 else {
170 return false;
171 };
172 current_prefix == prefix
173 && (current_folder == folder || legacy_parent == Some(current_folder))
174 })
175 });
176 let found = matches.next()?;
177 matches.next().is_none().then_some(found.id)
178}
179
180struct ReferenceIndex {
183 current: BTreeMap<EntityRef, (Id, usize)>,
184}
185
186impl ReferenceIndex {
187 fn new(catalog: &Catalog, kinds: BTreeSet<EntityKind>) -> Self {
188 let mut current: BTreeMap<EntityRef, (Id, usize)> = BTreeMap::new();
189 for kind in kinds {
190 let ids: Vec<_> = match kind {
191 EntityKind::Track => catalog.tracks.iter().map(|row| row.id).collect(),
192 EntityKind::Release => catalog.releases.iter().map(|row| row.id).collect(),
193 EntityKind::Artist => catalog.artists.iter().map(|row| row.id).collect(),
194 EntityKind::Recording => catalog.recordings.iter().map(|row| row.id).collect(),
195 EntityKind::Work => catalog.works.iter().map(|row| row.id).collect(),
196 EntityKind::ReleaseGroup => {
197 catalog.release_groups.iter().map(|row| row.id).collect()
198 }
199 EntityKind::Label => catalog.labels.iter().map(|row| row.id).collect(),
200 EntityKind::Genre => catalog.genres.iter().map(|row| row.id).collect(),
201 };
202 for id in ids {
203 if let Some(reference) = EntityRef::of(catalog, kind, id) {
204 current
205 .entry(reference)
206 .and_modify(|value| value.1 += 1)
207 .or_insert((id, 1));
208 }
209 }
210 if kind == EntityKind::Artist {
211 let mut by_mbid = BTreeMap::<&str, (Id, usize)>::new();
212 for artist in &catalog.artists {
213 if let Some(mbid) = artist.mbid.as_deref() {
214 by_mbid
215 .entry(mbid)
216 .and_modify(|row| row.1 += 1)
217 .or_insert((artist.id, 1));
218 }
219 }
220 for (mbid, (id, count)) in by_mbid {
221 if count == 1 {
222 current
225 .entry(EntityRef::new(kind, format!("mbid:{mbid}")))
226 .or_insert((id, 1));
227 }
228 }
229 }
230 if kind == EntityKind::Recording {
231 for recording in &catalog.recordings {
232 for track in &recording.track_ids {
233 if let Some(file) = catalog
234 .track(*track)
235 .and_then(|track| catalog.file(track.file_id))
236 {
237 current
238 .entry(EntityRef::new(kind, format!("local:{}", file.path)))
239 .or_insert((recording.id, 1));
240 }
241 }
242 }
243 }
244 }
245 Self { current }
246 }
247
248 fn resolve(&self, target: &EntityRef) -> Option<Id> {
249 if let Some((id, _)) = self.current.get(target) {
250 return Some(*id);
251 }
252 if target.kind != EntityKind::Release {
253 return None;
254 }
255 let (prefix, folder) = target.key.rsplit_once(RELEASE_KEY_SEPARATOR)?;
256 text::disc_folder(text::file_name(folder))?;
257 let parent = EntityRef::new(
258 EntityKind::Release,
259 format!("{prefix}{RELEASE_KEY_SEPARATOR}{}", text::folder(folder)),
260 );
261 self.current
262 .get(&parent)
263 .and_then(|(id, count)| (*count == 1).then_some(*id))
264 }
265}
266
267pub fn reconcile(data: &mut UserData, catalog: &Catalog) -> Attachment {
278 let mut report = Attachment::default();
279 let mut files_by_name: BTreeMap<&str, Vec<&AudioFile>> = BTreeMap::new();
280 for file in &catalog.files {
281 files_by_name
282 .entry(file.file_name())
283 .or_default()
284 .push(file);
285 }
286 let identities: BTreeMap<_, _> = data
287 .track_identities
288 .iter()
289 .map(|identity| (identity.path.as_str(), identity))
290 .collect();
291 let existing: BTreeSet<_> = data
292 .annotations
293 .iter()
294 .map(|a| (a.owner.clone(), a.target.clone()))
295 .chain(
296 data.plays
297 .iter()
298 .map(|p| (p.owner.clone(), p.track.clone())),
299 )
300 .chain(
301 data.counts
302 .iter()
303 .map(|c| (c.owner.clone(), c.track.clone())),
304 )
305 .chain(
306 data.scrobbles
307 .iter()
308 .map(|p| (p.owner.clone(), p.track.clone())),
309 )
310 .chain(data.playlists.iter().flat_map(|p| {
311 p.tracks
312 .iter()
313 .map(|track| (p.owner.clone(), track.clone()))
314 }))
315 .chain(data.relation_annotations.iter().flat_map(|a| {
316 [
317 (a.owner.clone(), a.relation.source.clone()),
318 (a.owner.clone(), a.relation.target.clone()),
319 ]
320 }))
321 .collect();
322 let references = ReferenceIndex::new(
323 catalog,
324 existing.iter().map(|(_, target)| target.kind).collect(),
325 );
326 let undone: BTreeSet<_> = data
327 .relinks
328 .iter()
329 .filter(|event| event.undone_at.is_some())
330 .map(|event| (&event.owner, &event.from))
331 .collect();
332 let mut moves = BTreeMap::new();
333 for (owner, target) in &existing {
334 let next = if let Some(id) = references.resolve(target) {
335 EntityRef::of(catalog, target.kind, id)
336 } else if target.kind == EntityKind::Track {
337 if undone.contains(&(owner, target)) {
338 continue;
339 }
340 identities.get(target.key.as_str()).and_then(|identity| {
341 let mut matches = files_by_name
342 .get(text::file_name(&target.key))?
343 .iter()
344 .filter(|file| identity.matches(file));
345 let candidate = matches.next()?;
346 matches
347 .next()
348 .is_none()
349 .then(|| EntityRef::new(EntityKind::Track, &candidate.path))
350 })
351 } else {
352 None
353 };
354 if let Some(next) = next
355 && next != *target
356 && !existing.contains(&(owner.clone(), next.clone()))
357 {
358 moves.insert((owner.clone(), target.clone()), next);
359 }
360 }
361 let mut destinations = BTreeMap::new();
364 for ((owner, _), to) in &moves {
365 *destinations
366 .entry((owner.clone(), to.clone()))
367 .or_insert(0usize) += 1;
368 }
369 moves.retain(|(owner, _), to| destinations.get(&(owner.clone(), to.clone())) == Some(&1));
370 for (owner, target) in data
371 .annotations
372 .iter_mut()
373 .map(|a| (&a.owner, &mut a.target))
374 .chain(data.plays.iter_mut().map(|p| (&p.owner, &mut p.track)))
375 .chain(data.counts.iter_mut().map(|c| (&c.owner, &mut c.track)))
376 .chain(data.scrobbles.iter_mut().map(|p| (&p.owner, &mut p.track)))
377 .chain(
378 data.playlists
379 .iter_mut()
380 .flat_map(|p| p.tracks.iter_mut().map(|track| (&p.owner, track))),
381 )
382 {
383 if let Some(next) = moves.get(&(owner.clone(), target.clone())) {
384 *target = next.clone();
385 report.moved += 1;
386 }
387 if references.resolve(target).is_some() {
388 report.attached += 1;
389 } else {
390 report.waiting += 1;
391 }
392 }
393 for annotation in &mut data.relation_annotations {
394 for endpoint in [
395 &mut annotation.relation.source,
396 &mut annotation.relation.target,
397 ] {
398 if let Some(next) = moves.get(&(annotation.owner.clone(), endpoint.clone())) {
399 *endpoint = next.clone();
400 }
401 }
402 }
403 let paths: BTreeSet<_> = data
404 .annotations
405 .iter()
406 .map(|a| &a.target)
407 .chain(data.plays.iter().map(|p| &p.track))
408 .chain(data.counts.iter().map(|c| &c.track))
409 .chain(data.scrobbles.iter().map(|p| &p.track))
410 .chain(data.playlists.iter().flat_map(|p| &p.tracks))
411 .chain(
412 data.relation_annotations
413 .iter()
414 .flat_map(|a| [&a.relation.source, &a.relation.target]),
415 )
416 .filter(|target| target.kind == EntityKind::Track)
417 .map(|target| target.key.as_str())
418 .collect();
419 let mut identities: BTreeMap<_, _> = std::mem::take(&mut data.track_identities)
420 .into_iter()
421 .map(|identity| (identity.path.clone(), identity))
422 .collect();
423 for file in catalog
424 .files
425 .iter()
426 .filter(|file| paths.contains(file.path.as_str()))
427 {
428 identities.insert(file.path.clone(), TrackIdentity::of(file));
429 }
430 let relink_paths: BTreeSet<_> = data
431 .relinks
432 .iter()
433 .filter(|event| event.from.kind == EntityKind::Track)
434 .flat_map(|event| [&event.from.key, &event.to.key])
435 .collect();
436 data.track_identities = identities
437 .into_values()
438 .filter(|identity| {
439 paths.contains(identity.path.as_str()) || relink_paths.contains(&identity.path)
440 })
441 .collect();
442 report
443}
444
445#[cfg(test)]
446#[path = "user_identity_tests.rs"]
447mod tests;