1use std::collections::{BTreeMap, BTreeSet};
24
25use crate::model::{Catalog, EntityKind, Id};
26use crate::text;
27
28#[path = "user_history.rs"]
29mod history;
30#[path = "user_identity.rs"]
31mod identity;
32#[path = "user_preferences.rs"]
33mod preferences;
34pub use preferences::{ClientPreference, valid_preference};
35#[path = "user_playback.rs"]
36mod playback;
37#[path = "user_playlists.rs"]
38mod playlists;
39#[path = "user_relink.rs"]
40mod relink;
41pub use identity::{Attachment, TrackIdentity, reconcile};
42pub use playback::{
43 PLAYBACK_ENTRY_LIMIT, PLAYBACK_PROFILE_LIMIT, PLAYBACK_STATE_LIMIT, PlaybackEntry,
44 PlaybackSettings, PlaybackSource, PlaybackState, valid_playback_profile,
45};
46pub use playlists::{
47 PLAYLIST_LIMIT, PLAYLIST_OWNER_LIMIT, PLAYLIST_TRACK_LIMIT, Playlist, Scrobble,
48};
49pub use relink::{Relink, RelinkSummary};
50
51pub const USER_FORMAT_VERSION: u32 = 2;
57
58pub const USER_FILE: &str = "user.json";
60
61pub type UserRef = String;
67
68pub const LOCAL_USER: &str = aede_accounts::LOCAL_OWNER;
70
71pub const RELEASE_KEY_SEPARATOR: char = '|';
73
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
82pub struct EntityRef {
83 pub kind: EntityKind,
85 pub key: String,
87}
88
89impl EntityRef {
90 pub fn new(kind: EntityKind, key: impl Into<String>) -> EntityRef {
92 EntityRef {
93 kind,
94 key: key.into(),
95 }
96 }
97
98 pub fn to_token(&self) -> String {
103 format!("{}:{}", self.kind.as_str(), self.key)
104 }
105
106 pub fn parse_token(text: &str) -> Option<EntityRef> {
108 let (kind, key) = text.split_once(':')?;
109 Some(EntityRef {
110 kind: EntityKind::parse_kind(kind)?,
111 key: key.to_string(),
112 })
113 }
114
115 pub fn of(catalog: &Catalog, kind: EntityKind, id: Id) -> Option<EntityRef> {
117 let key = match kind {
118 EntityKind::Track => catalog
121 .track(id)
122 .and_then(|t| catalog.file(t.file_id))
123 .map(|f| f.path.clone())?,
124 EntityKind::Release => {
127 let release = catalog.release(id)?;
128 let artist = release
129 .album_artist_id
130 .and_then(|a| catalog.artist(a))
131 .map(|a| a.key.clone())
132 .unwrap_or_default();
133 let folder = &release.folder;
134 format!(
135 "{artist}{RELEASE_KEY_SEPARATOR}{}{RELEASE_KEY_SEPARATOR}{folder}",
136 text::normalize(&release.title)
137 )
138 }
139 EntityKind::Artist => catalog.artist(id).map(|a| a.key.clone())?,
140 EntityKind::Recording => {
141 let recording = catalog.recording(id)?;
142 recording.mbid.clone().or_else(|| {
143 recording
144 .track_ids
145 .first()
146 .and_then(|track| catalog.track(*track))
147 .and_then(|track| catalog.file(track.file_id))
148 .map(|file| format!("local:{}", file.path))
149 })?
150 }
151 EntityKind::Work => catalog.work(id).map(|w| w.mbid.clone())?,
152 EntityKind::ReleaseGroup => catalog.release_group(id).map(|g| g.mbid.clone())?,
153 EntityKind::Label => catalog.label(id).map(|l| l.key.clone())?,
154 EntityKind::Genre => catalog.genres.get(id as usize).map(|g| g.key.clone())?,
155 };
156 Some(EntityRef { kind, key })
157 }
158
159 pub fn resolve(&self, catalog: &Catalog) -> Option<Id> {
161 match self.kind {
162 EntityKind::Track => catalog
163 .tracks
164 .iter()
165 .find(|t| catalog.file(t.file_id).is_some_and(|f| f.path == self.key))
166 .map(|t| t.id),
167 EntityKind::Release => identity::resolve_release(self, catalog),
168 EntityKind::Artist => catalog
169 .artists
170 .iter()
171 .find(|a| a.key == self.key)
172 .or_else(|| {
173 let mbid = self.key.strip_prefix("mbid:")?;
174 let mut matched = catalog
175 .artists
176 .iter()
177 .filter(|artist| artist.mbid.as_deref() == Some(mbid));
178 let artist = matched.next()?;
179 matched.next().is_none().then_some(artist)
180 })
181 .map(|a| a.id),
182 EntityKind::Recording => catalog
183 .recordings
184 .iter()
185 .find(|recording| {
186 recording.mbid.as_deref() == Some(&self.key)
187 || self.key.strip_prefix("local:").is_some_and(|path| {
188 recording.track_ids.iter().any(|track| {
189 catalog
190 .track(*track)
191 .and_then(|track| catalog.file(track.file_id))
192 .is_some_and(|file| file.path == path)
193 })
194 })
195 })
196 .map(|recording| recording.id),
197 EntityKind::Work => catalog
198 .works
199 .iter()
200 .find(|work| work.mbid == self.key)
201 .map(|work| work.id),
202 EntityKind::ReleaseGroup => catalog
203 .release_groups
204 .iter()
205 .find(|group| group.mbid == self.key)
206 .map(|group| group.id),
207 EntityKind::Label => catalog
208 .labels
209 .iter()
210 .find(|l| l.key == self.key)
211 .map(|l| l.id),
212 EntityKind::Genre => catalog
213 .genres
214 .iter()
215 .find(|g| g.key == self.key)
216 .map(|g| g.id),
217 }
218 }
219
220 pub fn display_name(&self, catalog: &Catalog) -> String {
222 let named = self.resolve(catalog).and_then(|id| match self.kind {
223 EntityKind::Track => catalog.track(id).map(|t| t.title.clone()),
224 EntityKind::Release => catalog.release(id).map(|r| r.title.clone()),
225 EntityKind::Artist => catalog.artist(id).map(|a| a.name.clone()),
226 EntityKind::Recording => catalog.recording(id).map(|r| r.title.clone()),
227 EntityKind::Work => catalog.work(id).map(|w| w.title.clone()),
228 EntityKind::ReleaseGroup => catalog.release_group(id).map(|g| g.title.clone()),
229 EntityKind::Label => catalog.label(id).map(|l| l.name.clone()),
230 EntityKind::Genre => catalog.genres.get(id as usize).map(|g| g.name.clone()),
231 });
232 named.unwrap_or_else(|| match self.kind {
233 EntityKind::Track => text::file_name(&self.key).to_string(),
235 _ => self.key.clone(),
236 })
237 }
238}
239
240enum RelationAnnotationMatch {
241 Absent,
242 Unique(usize),
243 Ambiguous,
244}
245
246pub(crate) fn same_annotation_relation(
247 catalog: &Catalog,
248 held: &crate::graph::RelationRef,
249 current: &crate::graph::RelationRef,
250) -> bool {
251 held == current
252 || (current.kind.starts_with("credit:")
253 && crate::graph::same_credit_relation(catalog, held, current))
254}
255
256#[derive(Debug, Clone, Default, PartialEq)]
263pub struct Annotation {
264 pub owner: UserRef,
266 pub target: EntityRef,
268 pub loved: bool,
270 pub rating: Option<u8>,
272 pub note: Option<String>,
274 pub tags: BTreeSet<String>,
276 pub created_at: u64,
278 pub updated_at: u64,
280}
281
282impl Default for EntityRef {
283 fn default() -> EntityRef {
284 EntityRef {
285 kind: EntityKind::Track,
286 key: String::new(),
287 }
288 }
289}
290
291impl Annotation {
292 pub fn is_empty(&self) -> bool {
298 !self.loved
305 && self.rating.is_none()
306 && self.note.as_deref().is_none_or(|n| n.trim().is_empty())
307 && self.tags.is_empty()
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct RelationAnnotation {
319 pub owner: UserRef,
321 pub relation: crate::graph::RelationRef,
323 pub note: Option<String>,
325 pub tags: BTreeSet<String>,
327 pub created_at: u64,
329 pub updated_at: u64,
331}
332
333impl RelationAnnotation {
334 pub fn is_empty(&self) -> bool {
336 self.note
337 .as_deref()
338 .is_none_or(|note| note.trim().is_empty())
339 && self.tags.is_empty()
340 }
341}
342
343#[derive(Debug, Clone, PartialEq)]
347pub struct Play {
348 pub owner: UserRef,
350 pub track: EntityRef,
352 pub at: u64,
354 pub ms_played: u64,
356 pub completed: bool,
361}
362
363#[derive(Debug, Clone, Default, PartialEq)]
370pub struct PlayCount {
371 pub owner: UserRef,
373 pub track: EntityRef,
375 pub count: u32,
377 pub completed_count: u32,
379 pub last_played: u64,
381}
382
383#[derive(Debug, Clone, PartialEq)]
391pub struct Collection {
392 pub owner: UserRef,
394 pub name: String,
396 pub expression: String,
399 pub created_at: u64,
401 pub updated_at: u64,
403}
404
405pub const HISTORY_LIMIT: usize = 500;
412pub const HISTORY_STORE_LIMIT: usize = HISTORY_LIMIT * aede_accounts::MAX_ACCOUNTS;
415pub const USER_MAX_BYTES: usize = 64 * 1024 * 1024;
417
418#[derive(Debug, Clone, PartialEq, Eq)]
432pub struct SetAside {
433 pub owner: UserRef,
435 pub release_group: String,
440 pub title: String,
446 pub created_at: u64,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
469pub struct SameArtist {
470 pub owner: UserRef,
472 pub spelling: String,
474 pub filed_as: String,
476 pub created_at: u64,
478}
479
480#[derive(Debug, Clone, Default)]
482pub struct UserData {
483 pub client_preferences: Vec<ClientPreference>,
485 pub annotations: Vec<Annotation>,
487 pub relation_annotations: Vec<RelationAnnotation>,
489 pub plays: Vec<Play>,
491 pub counts: Vec<PlayCount>,
493 pub collections: Vec<Collection>,
495 pub playlists: Vec<Playlist>,
497 pub scrobbles: Vec<Scrobble>,
499 pub playback_states: Vec<PlaybackState>,
501 pub set_aside: Vec<SetAside>,
503 pub same_artist: Vec<SameArtist>,
505 pub track_identities: Vec<TrackIdentity>,
507 pub relinks: Vec<Relink>,
509}
510
511impl UserData {
512 pub fn find(&self, owner: &str, target: &EntityRef) -> Option<&Annotation> {
514 self.annotations
515 .iter()
516 .find(|a| a.owner == owner && &a.target == target)
517 }
518
519 pub fn entry(&mut self, owner: &str, target: &EntityRef, now: u64) -> &mut Annotation {
524 let found = self
525 .annotations
526 .iter()
527 .position(|a| a.owner == owner && &a.target == target);
528 let at = match found {
529 Some(at) => at,
530 None => {
531 self.annotations.push(Annotation {
532 owner: owner.to_string(),
533 target: target.clone(),
534 created_at: now,
535 updated_at: now,
536 ..Default::default()
537 });
538 self.annotations.len() - 1
539 }
540 };
541 &mut self.annotations[at]
542 }
543
544 pub fn forget_empty(&mut self) {
549 self.annotations.retain(|a| !a.is_empty());
550 self.relation_annotations.retain(|a| !a.is_empty());
551 }
552
553 pub fn find_relation(
555 &self,
556 owner: &str,
557 relation: &crate::graph::RelationRef,
558 ) -> Option<&RelationAnnotation> {
559 self.relation_annotations
560 .iter()
561 .find(|annotation| annotation.owner == owner && &annotation.relation == relation)
562 }
563
564 pub fn find_relation_in(
570 &self,
571 catalog: &Catalog,
572 owner: &str,
573 relation: &crate::graph::RelationRef,
574 ) -> Option<&RelationAnnotation> {
575 match self.relation_annotation_match(catalog, owner, relation) {
576 RelationAnnotationMatch::Unique(index) => self.relation_annotations.get(index),
577 RelationAnnotationMatch::Absent | RelationAnnotationMatch::Ambiguous => None,
578 }
579 }
580
581 pub fn relation_entry_in(
584 &mut self,
585 catalog: &Catalog,
586 owner: &str,
587 relation: &crate::graph::RelationRef,
588 now: u64,
589 ) -> Result<&mut RelationAnnotation, String> {
590 match self.relation_annotation_match(catalog, owner, relation) {
591 RelationAnnotationMatch::Unique(index) => Ok(&mut self.relation_annotations[index]),
592 RelationAnnotationMatch::Ambiguous => Err(
593 "several saved annotations match this credit; use their exact saved references"
594 .into(),
595 ),
596 RelationAnnotationMatch::Absent => {
597 let reference = if relation.kind.starts_with("credit:") {
598 crate::graph::canonical_credit_relation(catalog, relation.clone())
599 } else {
600 relation.clone()
601 };
602 Ok(self.relation_entry(owner, &reference, now))
603 }
604 }
605 }
606
607 pub fn remove_relation_in(
610 &mut self,
611 catalog: &Catalog,
612 owner: &str,
613 relation: &crate::graph::RelationRef,
614 ) -> Result<bool, String> {
615 match self.relation_annotation_match(catalog, owner, relation) {
616 RelationAnnotationMatch::Unique(index) => {
617 self.relation_annotations.remove(index);
618 Ok(true)
619 }
620 RelationAnnotationMatch::Absent => Ok(false),
621 RelationAnnotationMatch::Ambiguous => Err(
622 "several saved annotations match this credit; use their exact saved references"
623 .into(),
624 ),
625 }
626 }
627
628 fn relation_annotation_match(
629 &self,
630 catalog: &Catalog,
631 owner: &str,
632 relation: &crate::graph::RelationRef,
633 ) -> RelationAnnotationMatch {
634 if let Some(index) = self
635 .relation_annotations
636 .iter()
637 .position(|annotation| annotation.owner == owner && &annotation.relation == relation)
638 {
639 return RelationAnnotationMatch::Unique(index);
640 }
641 if !relation.kind.starts_with("credit:") {
642 return RelationAnnotationMatch::Absent;
643 }
644 let mut matching =
645 self.relation_annotations
646 .iter()
647 .enumerate()
648 .filter(|(_, annotation)| {
649 annotation.owner == owner
650 && same_annotation_relation(catalog, &annotation.relation, relation)
651 });
652 match (matching.next(), matching.next()) {
653 (None, _) => RelationAnnotationMatch::Absent,
654 (Some((index, _)), None) => RelationAnnotationMatch::Unique(index),
655 (Some(_), Some(_)) => RelationAnnotationMatch::Ambiguous,
656 }
657 }
658
659 pub fn relation_entry(
661 &mut self,
662 owner: &str,
663 relation: &crate::graph::RelationRef,
664 now: u64,
665 ) -> &mut RelationAnnotation {
666 let found = self
667 .relation_annotations
668 .iter()
669 .position(|annotation| annotation.owner == owner && &annotation.relation == relation);
670 let index = match found {
671 Some(index) => index,
672 None => {
673 self.relation_annotations.push(RelationAnnotation {
674 owner: owner.to_string(),
675 relation: relation.clone(),
676 note: None,
677 tags: BTreeSet::new(),
678 created_at: now,
679 updated_at: now,
680 });
681 self.relation_annotations.len() - 1
682 }
683 };
684 &mut self.relation_annotations[index]
685 }
686
687 pub fn copy(&mut self, owner: &str, from: &EntityRef, to: &EntityRef, now: u64) -> bool {
693 let Some(source) = self.find(owner, from).cloned() else {
694 return false;
695 };
696 let target = self.entry(owner, to, now);
697 target.loved = source.loved;
698 target.rating = source.rating;
699 target.note = source.note.clone();
700 target.tags = source.tags;
701 target.updated_at = now;
702 true
703 }
704
705 pub fn forget_last_play(&mut self, owner: &str, track: &EntityRef) -> bool {
717 let Some(index) = self
718 .plays
719 .iter()
720 .rposition(|p| p.owner == owner && &p.track == track)
721 else {
722 return false;
726 };
727 let completed = self.plays.remove(index).completed;
728 if let Some(counter) = self
729 .counts
730 .iter_mut()
731 .find(|c| c.owner == owner && &c.track == track)
732 {
733 counter.count = counter.count.saturating_sub(1);
734 counter.completed_count = counter.completed_count.saturating_sub(u32::from(completed));
735 counter.last_played = self
738 .plays
739 .iter()
740 .filter(|p| p.owner == owner && &p.track == track)
741 .map(|p| p.at)
742 .chain(
743 self.scrobbles
744 .iter()
745 .filter(|p| p.owner == owner && &p.track == track)
746 .map(|p| p.at_ms / 1000),
747 )
748 .max()
749 .unwrap_or(0);
750 }
751 self.counts
752 .retain(|c| c.count > 0 || c.owner != owner || &c.track != track);
753 true
754 }
755
756 pub fn forget_history(&mut self, owner: &str) -> (usize, usize) {
763 let plays = self.plays.iter().filter(|p| p.owner == owner).count()
764 + self.scrobbles.iter().filter(|p| p.owner == owner).count();
765 let counts = self.counts.iter().filter(|c| c.owner == owner).count();
766 self.plays.retain(|p| p.owner != owner);
767 self.scrobbles.retain(|p| p.owner != owner);
768 self.counts.retain(|c| c.owner != owner);
769 (plays, counts)
770 }
771
772 pub fn record_play(&mut self, play: Play) {
777 match self
778 .counts
779 .iter_mut()
780 .find(|c| c.owner == play.owner && c.track == play.track)
781 {
782 Some(counter) => {
783 counter.count = counter.count.saturating_add(1);
784 counter.completed_count = counter
785 .completed_count
786 .saturating_add(u32::from(play.completed));
787 counter.last_played = counter.last_played.max(play.at);
788 }
789 None => self.counts.push(PlayCount {
790 owner: play.owner.clone(),
791 track: play.track.clone(),
792 count: 1,
793 completed_count: u32::from(play.completed),
794 last_played: play.at,
795 }),
796 }
797 self.plays.push(play);
798 self.plays.sort_by_key(|play| play.at);
801 history::retain(&mut self.plays, |play| &play.owner);
803 }
804
805 pub fn collection(&self, owner: &str, name: &str) -> Option<&Collection> {
809 let wanted = text::normalize(name);
810 self.collections
811 .iter()
812 .find(|c| c.owner == owner && text::normalize(&c.name) == wanted)
813 }
814
815 pub fn save_collection(&mut self, owner: &str, name: &str, expression: &str, now: u64) -> bool {
821 let wanted = text::normalize(name);
822 if let Some(existing) = self
823 .collections
824 .iter_mut()
825 .find(|c| c.owner == owner && text::normalize(&c.name) == wanted)
826 {
827 existing.expression = expression.to_string();
828 existing.updated_at = now;
829 return true;
830 }
831 self.collections.push(Collection {
832 owner: owner.to_string(),
833 name: name.to_string(),
834 expression: expression.to_string(),
835 created_at: now,
836 updated_at: now,
837 });
838 false
839 }
840
841 pub fn forget_collection(&mut self, owner: &str, name: &str) -> bool {
843 let wanted = text::normalize(name);
844 let before = self.collections.len();
845 self.collections
846 .retain(|c| !(c.owner == owner && text::normalize(&c.name) == wanted));
847 self.collections.len() != before
848 }
849
850 pub fn play_count(&self, owner: &str, track: &EntityRef) -> u32 {
852 self.counts
853 .iter()
854 .find(|c| c.owner == owner && &c.track == track)
855 .map(|c| c.count)
856 .unwrap_or(0)
857 }
858}
859
860pub fn user_path(data_dir: &std::path::Path) -> std::path::PathBuf {
866 data_dir.join(USER_FILE)
867}
868
869pub fn save(data: &UserData, path: &std::path::Path) -> Result<(), crate::store::StoreError> {
878 preferences::validate(&data.client_preferences)?;
879 history::validate(data)?;
880 playlists::validate(data)?;
881 playback::validate(data)?;
882 let text = to_json(data)
883 .to_string_pretty_bounded(USER_MAX_BYTES)
884 .map_err(|_| crate::store::StoreError::Invalid("personal store byte limit exceeded"))?;
885 crate::atomic_file::write(path, text.as_bytes())?;
886 Ok(())
887}
888
889pub fn load(path: &std::path::Path) -> Result<Option<UserData>, crate::store::StoreError> {
891 use std::io::Read;
892 let file = match std::fs::File::open(path) {
893 Ok(file) => file,
894 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
895 Err(error) => return Err(error.into()),
896 };
897 let mut bytes = Vec::new();
898 file.take(USER_MAX_BYTES as u64 + 1)
899 .read_to_end(&mut bytes)?;
900 if bytes.len() > USER_MAX_BYTES {
901 return Err(crate::store::StoreError::Invalid(
902 "personal store byte limit exceeded",
903 ));
904 }
905 let text = String::from_utf8(bytes).map_err(|failure| {
906 std::io::Error::new(std::io::ErrorKind::InvalidData, failure.utf8_error())
907 })?;
908 let value = crate::json::parse(&text).map_err(crate::store::StoreError::Parse)?;
909 from_json(&value).map(Some)
910}
911
912pub fn to_json(data: &UserData) -> crate::json::Json {
914 use crate::json::Json;
915 let mut root = Json::obj();
916 root.set("format_version", USER_FORMAT_VERSION.into());
917
918 let annotations: Vec<Json> = data
919 .annotations
920 .iter()
921 .map(|a| {
922 let mut o = Json::obj();
923 o.set("owner", a.owner.as_str().into());
924 o.set("target", a.target.to_token().into());
925 if a.loved {
926 o.set("loved", Json::Bool(true));
927 }
928 if let Some(rating) = a.rating {
929 o.set("rating", u32::from(rating).into());
930 }
931 if let Some(note) = a.note.as_deref().filter(|n| !n.trim().is_empty()) {
936 o.set("note", note.into());
937 }
938 if !a.tags.is_empty() {
939 o.set(
940 "tags",
941 Json::Arr(a.tags.iter().map(|t| t.as_str().into()).collect()),
942 );
943 }
944 o.set("created_at", a.created_at.into());
945 o.set("updated_at", a.updated_at.into());
946 o
947 })
948 .collect();
949 root.set("annotations", Json::Arr(annotations));
950
951 let relation_annotations: Vec<Json> = data
952 .relation_annotations
953 .iter()
954 .map(|annotation| {
955 let mut row = Json::obj();
956 row.set("owner", annotation.owner.as_str().into());
957 row.set("source", annotation.relation.source.to_token().into());
958 row.set("relation", annotation.relation.kind.as_str().into());
959 row.set("target", annotation.relation.target.to_token().into());
960 row.set("provenance", annotation.relation.provenance.as_str().into());
961 if let Some(source_id) = &annotation.relation.source_id {
962 row.set("source_id", source_id.as_str().into());
963 }
964 if let Some(note) = annotation
965 .note
966 .as_deref()
967 .filter(|note| !note.trim().is_empty())
968 {
969 row.set("note", note.into());
970 }
971 if !annotation.tags.is_empty() {
972 row.set(
973 "tags",
974 Json::Arr(
975 annotation
976 .tags
977 .iter()
978 .map(|tag| tag.as_str().into())
979 .collect(),
980 ),
981 );
982 }
983 row.set("created_at", annotation.created_at.into());
984 row.set("updated_at", annotation.updated_at.into());
985 row
986 })
987 .collect();
988 root.set("relation_annotations", Json::Arr(relation_annotations));
989
990 let plays: Vec<Json> = data
991 .plays
992 .iter()
993 .map(|p| {
994 let mut o = Json::obj();
995 o.set("owner", p.owner.as_str().into());
996 o.set("track", p.track.to_token().into());
997 o.set("at", p.at.into());
998 o.set("ms_played", p.ms_played.into());
999 o.set("completed", Json::Bool(p.completed));
1000 o
1001 })
1002 .collect();
1003 root.set("plays", Json::Arr(plays));
1004
1005 let counts: Vec<Json> = data
1006 .counts
1007 .iter()
1008 .map(|c| {
1009 let mut o = Json::obj();
1010 o.set("owner", c.owner.as_str().into());
1011 o.set("track", c.track.to_token().into());
1012 o.set("count", c.count.into());
1013 o.set("completed_count", c.completed_count.into());
1014 o.set("last_played", c.last_played.into());
1015 o
1016 })
1017 .collect();
1018 root.set("counts", Json::Arr(counts));
1019
1020 let collections: Vec<Json> = data
1021 .collections
1022 .iter()
1023 .map(|c| {
1024 let mut o = Json::obj();
1025 o.set("owner", c.owner.as_str().into());
1026 o.set("name", c.name.as_str().into());
1027 o.set("query", c.expression.as_str().into());
1028 o.set("created_at", c.created_at.into());
1029 o.set("updated_at", c.updated_at.into());
1030 o
1031 })
1032 .collect();
1033 root.set("collections", Json::Arr(collections));
1034 playlists::write_tables(data, &mut root);
1035 playback::write_table(data, &mut root);
1036 preferences::write(data, &mut root);
1037
1038 let set_aside: Vec<Json> = data
1039 .set_aside
1040 .iter()
1041 .map(|a| {
1042 let mut o = Json::obj();
1043 o.set("owner", a.owner.as_str().into());
1044 o.set("release_group", a.release_group.as_str().into());
1045 o.set("title", a.title.as_str().into());
1046 o.set("created_at", a.created_at.into());
1047 o
1048 })
1049 .collect();
1050 root.set("set_aside", Json::Arr(set_aside));
1051
1052 let same_artist: Vec<Json> = data
1053 .same_artist
1054 .iter()
1055 .map(|m| {
1056 let mut o = Json::obj();
1057 o.set("owner", m.owner.as_str().into());
1058 o.set("spelling", m.spelling.as_str().into());
1059 o.set("filed_as", m.filed_as.as_str().into());
1060 o.set("created_at", m.created_at.into());
1061 o
1062 })
1063 .collect();
1064 root.set("same_artist", Json::Arr(same_artist));
1065 root.set(
1066 "track_identities",
1067 Json::Arr(
1068 data.track_identities
1069 .iter()
1070 .map(TrackIdentity::to_json)
1071 .collect(),
1072 ),
1073 );
1074 root.set(
1075 "relinks",
1076 Json::Arr(data.relinks.iter().map(Relink::to_json).collect()),
1077 );
1078 root
1079}
1080
1081pub fn from_json(value: &crate::json::Json) -> Result<UserData, crate::store::StoreError> {
1093 value
1094 .check_pretty_size(USER_MAX_BYTES)
1095 .map_err(|_| crate::store::StoreError::Invalid("personal store byte limit exceeded"))?;
1096 use crate::store::StoreError;
1097 let found = value.field_u32("format_version").unwrap_or(0);
1098 if !matches!(found, 1 | USER_FORMAT_VERSION) {
1099 return Err(StoreError::Version {
1100 found,
1101 expected: USER_FORMAT_VERSION,
1102 });
1103 }
1104 for (table, message) in [
1105 ("annotations", "user annotations must be an array"),
1106 (
1107 "relation_annotations",
1108 "user relation annotations must be an array",
1109 ),
1110 ("plays", "user plays must be an array"),
1111 ("counts", "user counts must be an array"),
1112 ("collections", "user collections must be an array"),
1113 ("playlists", "user playlists must be an array"),
1114 ("scrobbles", "user scrobbles must be an array"),
1115 ("playback_states", "user playback states must be an array"),
1116 ("set_aside", "user set-aside decisions must be an array"),
1117 ("same_artist", "user artist decisions must be an array"),
1118 ("track_identities", "user track identities must be an array"),
1119 ("relinks", "user relink history must be an array"),
1120 ] {
1121 if value
1122 .get(table)
1123 .is_some_and(|value| value.as_arr().is_none())
1124 {
1125 return Err(StoreError::Invalid(message));
1126 }
1127 }
1128 let mut data = UserData {
1129 client_preferences: preferences::read(value)?,
1130 playlists: playlists::read_playlists(value)?,
1131 scrobbles: playlists::read_scrobbles(value)?,
1132 playback_states: playback::read_states(value)?,
1133 track_identities: value
1134 .get("track_identities")
1135 .and_then(crate::json::Json::as_arr)
1136 .unwrap_or(&[])
1137 .iter()
1138 .filter_map(TrackIdentity::from_json)
1139 .collect(),
1140 relinks: value
1141 .get("relinks")
1142 .and_then(crate::json::Json::as_arr)
1143 .unwrap_or(&[])
1144 .iter()
1145 .filter_map(Relink::from_json)
1146 .collect(),
1147 ..Default::default()
1148 };
1149
1150 for row in value
1151 .get("annotations")
1152 .and_then(|v| v.as_arr())
1153 .unwrap_or(&[])
1154 {
1155 let Some(target) = row
1156 .field_str("target")
1157 .and_then(|t| EntityRef::parse_token(&t))
1158 else {
1159 continue;
1160 };
1161 data.annotations.push(Annotation {
1162 owner: row.field_str("owner").unwrap_or_else(|| LOCAL_USER.into()),
1163 target,
1164 loved: row.field_bool("loved"),
1165 rating: row.field_u32("rating").map(|r| r.clamp(1, 5) as u8),
1166 note: row.field_str("note"),
1167 tags: row
1168 .get("tags")
1169 .and_then(|v| v.as_arr())
1170 .map(|list| list.iter().filter_map(|t| t.as_string()).collect())
1171 .unwrap_or_default(),
1172 created_at: row.field_u64("created_at").unwrap_or(0),
1173 updated_at: row.field_u64("updated_at").unwrap_or(0),
1174 });
1175 }
1176
1177 for row in value
1178 .get("relation_annotations")
1179 .and_then(|value| value.as_arr())
1180 .unwrap_or(&[])
1181 {
1182 let (Some(source), Some(kind), Some(target), Some(provenance)) = (
1183 row.field_str("source")
1184 .and_then(|value| EntityRef::parse_token(&value)),
1185 row.field_str("relation"),
1186 row.field_str("target")
1187 .and_then(|value| EntityRef::parse_token(&value)),
1188 row.field_str("provenance"),
1189 ) else {
1190 continue;
1191 };
1192 let annotation = RelationAnnotation {
1193 owner: row.field_str("owner").unwrap_or_else(|| LOCAL_USER.into()),
1194 relation: crate::graph::RelationRef {
1195 source,
1196 kind,
1197 target,
1198 provenance,
1199 source_id: row.field_str("source_id"),
1200 },
1201 note: row.field_str("note"),
1202 tags: row
1203 .get("tags")
1204 .and_then(|value| value.as_arr())
1205 .map(|values| {
1206 values
1207 .iter()
1208 .filter_map(|value| value.as_string())
1209 .collect()
1210 })
1211 .unwrap_or_default(),
1212 created_at: row.field_u64("created_at").unwrap_or(0),
1213 updated_at: row.field_u64("updated_at").unwrap_or(0),
1214 };
1215 if !annotation.is_empty() {
1216 data.relation_annotations.push(annotation);
1217 }
1218 }
1219
1220 for row in value.get("plays").and_then(|v| v.as_arr()).unwrap_or(&[]) {
1221 let Some(track) = row
1222 .field_str("track")
1223 .and_then(|t| EntityRef::parse_token(&t))
1224 else {
1225 continue;
1226 };
1227 data.plays.push(Play {
1228 owner: row.field_str("owner").unwrap_or_else(|| LOCAL_USER.into()),
1229 track,
1230 at: row.field_u64("at").unwrap_or(0),
1231 ms_played: row.field_u64("ms_played").unwrap_or(0),
1232 completed: row.field_bool("completed"),
1233 });
1234 }
1235
1236 let mut legacy_completed = BTreeMap::<(&str, &EntityRef), u32>::new();
1238 for play in data.plays.iter().filter(|play| play.completed) {
1239 let count = legacy_completed
1240 .entry((&play.owner, &play.track))
1241 .or_default();
1242 *count = count.saturating_add(1);
1243 }
1244 for row in value.get("counts").and_then(|v| v.as_arr()).unwrap_or(&[]) {
1245 let Some(track) = row
1246 .field_str("track")
1247 .and_then(|t| EntityRef::parse_token(&t))
1248 else {
1249 continue;
1250 };
1251 let owner = row.field_str("owner").unwrap_or_else(|| LOCAL_USER.into());
1252 let completed_count = match row.get("completed_count") {
1253 Some(value) => value.as_u32().ok_or(crate::store::StoreError::Invalid(
1254 "invalid completed listening count",
1255 ))?,
1256 None => legacy_completed
1257 .get(&(owner.as_str(), &track))
1258 .copied()
1259 .unwrap_or(0)
1260 .min(row.field_u32("count").unwrap_or(0)),
1261 };
1262 data.counts.push(PlayCount {
1263 owner,
1264 track,
1265 count: row.field_u32("count").unwrap_or(0),
1266 completed_count,
1267 last_played: row.field_u64("last_played").unwrap_or(0),
1268 });
1269 }
1270 for row in value
1271 .get("collections")
1272 .and_then(|v| v.as_arr())
1273 .unwrap_or(&[])
1274 {
1275 let (Some(name), Some(expression)) = (row.field_str("name"), row.field_str("query")) else {
1276 continue;
1277 };
1278 data.collections.push(Collection {
1279 owner: row.field_str("owner").unwrap_or_else(|| LOCAL_USER.into()),
1280 name,
1281 expression,
1282 created_at: row.field_u64("created_at").unwrap_or(0),
1283 updated_at: row.field_u64("updated_at").unwrap_or(0),
1284 });
1285 }
1286 for row in value
1287 .get("set_aside")
1288 .and_then(crate::json::Json::as_arr)
1289 .unwrap_or(&[])
1290 {
1291 let Some(release_group) = row.field_str("release_group") else {
1295 continue;
1296 };
1297 data.set_aside.push(SetAside {
1298 owner: row
1299 .field_str("owner")
1300 .unwrap_or_else(|| LOCAL_USER.to_string()),
1301 release_group,
1302 title: row.field_str("title").unwrap_or_default(),
1303 created_at: row.field_u64("created_at").unwrap_or(0),
1304 });
1305 }
1306 for row in value
1307 .get("same_artist")
1308 .and_then(crate::json::Json::as_arr)
1309 .unwrap_or(&[])
1310 {
1311 let (Some(spelling), Some(filed_as)) =
1316 (row.field_str("spelling"), row.field_str("filed_as"))
1317 else {
1318 continue;
1319 };
1320 let (spelling, filed_as) = (text::normalize(&spelling), text::normalize(&filed_as));
1324 if spelling.is_empty() || filed_as.is_empty() || spelling == filed_as {
1325 continue;
1326 }
1327 data.same_artist.push(SameArtist {
1328 owner: row
1329 .field_str("owner")
1330 .unwrap_or_else(|| LOCAL_USER.to_string()),
1331 spelling,
1332 filed_as,
1333 created_at: row.field_u64("created_at").unwrap_or(0),
1334 });
1335 }
1336 data.plays.sort_by_key(|play| play.at);
1339 history::validate(&data)?;
1340 Ok(data)
1341}
1342
1343#[derive(Debug, Clone, Copy, Default, PartialEq)]
1345pub struct Merge {
1346 pub added: usize,
1348 pub updated: usize,
1350 pub kept: usize,
1352 pub plays: usize,
1354 pub collections: usize,
1356}
1357
1358pub fn merge(into: &mut UserData, incoming: UserData) -> Merge {
1376 let mut report = Merge::default();
1377 for entry in incoming.client_preferences {
1378 if !into
1379 .client_preferences
1380 .iter()
1381 .any(|known| known.owner == entry.owner && known.key == entry.key)
1382 {
1383 into.client_preferences.push(entry);
1384 }
1385 }
1386 playlists::merge(into, incoming.playlists, incoming.scrobbles, &mut report);
1387 playback::merge(into, incoming.playback_states, &mut report);
1388
1389 for identity in incoming.track_identities {
1392 if !into
1393 .track_identities
1394 .iter()
1395 .any(|known| known.path == identity.path)
1396 {
1397 into.track_identities.push(identity);
1398 }
1399 }
1400 for mut event in incoming.relinks {
1401 let known = into.relinks.iter().any(|known| {
1402 known.origin_id == event.origin_id
1403 && known.owner == event.owner
1404 && known.from == event.from
1405 && known.to == event.to
1406 && known.at == event.at
1407 && known.before.annotations == event.before.annotations
1408 && known.before.plays == event.before.plays
1409 && known.before.counts == event.before.counts
1410 && known.before.relation_annotations == event.before.relation_annotations
1411 && known.before.playlists == event.before.playlists
1412 && known.before.scrobbles == event.before.scrobbles
1413 });
1414 if known {
1415 continue;
1416 }
1417 if into.relinks.iter().any(|known| known.id == event.id) {
1418 let Some(next) = into
1419 .relinks
1420 .iter()
1421 .map(|known| known.id)
1422 .max()
1423 .unwrap_or(0)
1424 .checked_add(1)
1425 else {
1426 continue;
1427 };
1428 event.id = next;
1429 }
1430 into.relinks.push(event);
1431 }
1432
1433 for annotation in incoming.annotations {
1434 match into
1435 .annotations
1436 .iter_mut()
1437 .find(|a| a.owner == annotation.owner && a.target == annotation.target)
1438 {
1439 Some(existing) if existing.updated_at >= annotation.updated_at => report.kept += 1,
1440 Some(existing) => {
1441 *existing = annotation;
1442 report.updated += 1;
1443 }
1444 None => {
1445 into.annotations.push(annotation);
1446 report.added += 1;
1447 }
1448 }
1449 }
1450
1451 for annotation in incoming.relation_annotations {
1452 match into.relation_annotations.iter_mut().find(|existing| {
1453 existing.owner == annotation.owner && existing.relation == annotation.relation
1454 }) {
1455 Some(existing) if existing.updated_at >= annotation.updated_at => report.kept += 1,
1456 Some(existing) => {
1457 *existing = annotation;
1458 report.updated += 1;
1459 }
1460 None => {
1461 into.relation_annotations.push(annotation);
1462 report.added += 1;
1463 }
1464 }
1465 }
1466
1467 let play_key = |play: &Play| {
1471 (
1472 play.owner.clone(),
1473 play.track.clone(),
1474 play.at,
1475 play.ms_played,
1476 play.completed,
1477 )
1478 };
1479 let mut available = BTreeMap::new();
1480 for play in &into.plays {
1481 *available.entry(play_key(play)).or_insert(0usize) += 1;
1482 }
1483 for play in incoming.plays {
1484 let copies = available.entry(play_key(&play)).or_default();
1485 if *copies > 0 {
1486 *copies -= 1;
1487 } else {
1488 into.plays.push(play);
1489 report.plays += 1;
1490 }
1491 }
1492 into.plays.sort_by_key(|p| p.at);
1493 history::retain(&mut into.plays, |play| &play.owner);
1494
1495 for count in incoming.counts {
1496 match into
1497 .counts
1498 .iter_mut()
1499 .find(|c| c.owner == count.owner && c.track == count.track)
1500 {
1501 Some(existing) => {
1502 existing.count = existing.count.max(count.count);
1503 existing.completed_count = existing.completed_count.max(count.completed_count);
1504 existing.last_played = existing.last_played.max(count.last_played);
1505 }
1506 None => into.counts.push(count),
1507 }
1508 }
1509
1510 for collection in incoming.collections {
1511 let owner = collection.owner.clone();
1512 match into.collections.iter_mut().find(|c| {
1513 c.owner == owner && text::normalize(&c.name) == text::normalize(&collection.name)
1514 }) {
1515 Some(existing) if existing.updated_at >= collection.updated_at => report.kept += 1,
1516 Some(existing) => {
1517 *existing = collection;
1518 report.collections += 1;
1519 }
1520 None => {
1521 into.collections.push(collection);
1522 report.collections += 1;
1523 }
1524 }
1525 }
1526 for aside in incoming.set_aside {
1529 let known = into
1530 .set_aside
1531 .iter()
1532 .any(|a| a.owner == aside.owner && a.release_group == aside.release_group);
1533 if !known {
1534 into.set_aside.push(aside);
1535 report.added += 1;
1536 }
1537 }
1538 for same in incoming.same_artist {
1544 let known = into
1545 .same_artist
1546 .iter()
1547 .any(|m| m.owner == same.owner && m.spelling == same.spelling);
1548 if !known {
1549 into.same_artist.push(same);
1550 report.added += 1;
1551 }
1552 }
1553 report
1554}
1555
1556#[cfg(test)]
1557#[path = "user_test_support.rs"]
1558mod test_support;
1559
1560#[cfg(test)]
1561#[path = "user_tests.rs"]
1562mod tests;
1563
1564#[cfg(test)]
1565#[path = "user_history_tests.rs"]
1566mod history_tests;