1use std::collections::{BTreeMap, BTreeSet};
28
29use crate::model::{Catalog, EntityKind, Id};
30use crate::text;
31use crate::user::{EntityRef, UserData};
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum Query {
36 Browse(Box<browse::Filter>),
38 All,
40 And(Vec<Query>),
42 Or(Vec<Query>),
44 Not(Box<Query>),
46 Term(Term),
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct Term {
53 pub field: Field,
55 pub test: Test,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum Test {
62 Contains(String),
64 Is(String),
66 Compare(Compare, f64),
68 Between(Option<f64>, Option<f64>),
70 Set,
72 Unset,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Compare {
79 Equal,
81 Greater,
83 AtLeast,
85 Less,
87 AtMost,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum Field {
100 Title,
102 Artist,
104 Album,
106 Recording,
108 Work,
110 Movement,
112 ReleaseGroup,
114 AlbumArtist,
116 Genre,
118 Label,
120 Comment,
122 Lyrics,
125 Path,
127 Codec,
129 Container,
131 Year,
133 Duration,
135 Size,
137 Bitrate,
139 SampleRate,
141 BitDepth,
143 Channels,
145 Reference,
147 Lossless,
149 Compilation,
151 Rating(Scope),
153 Loved(Scope),
155 LovedAnywhere,
165 Tag(Scope),
167 Note(Scope),
169 Played,
171 Performing,
178 Credit(&'static str),
184 Instrument,
186 Guest,
188 CompilationArtist,
190 Contributor,
192 Collaborator,
194 Anything,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum Scope {
201 Track,
203 Album,
205 Artist,
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub struct QueryError {
212 pub message: String,
214}
215
216impl std::fmt::Display for QueryError {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 write!(f, "{}", self.message)
219 }
220}
221
222impl std::error::Error for QueryError {}
223
224fn error(message: impl Into<String>) -> QueryError {
225 QueryError {
226 message: message.into(),
227 }
228}
229
230mod parsing;
231pub use parsing::parse;
232
233const FIELD_NAMES: &[(&str, Field)] = &[
235 ("title", Field::Title),
236 ("artist", Field::Artist),
237 ("album", Field::Album),
238 ("albumartist", Field::AlbumArtist),
239 ("recording", Field::Recording),
240 ("work", Field::Work),
241 ("movement", Field::Movement),
242 ("releasegroup", Field::ReleaseGroup),
243 ("release-group", Field::ReleaseGroup),
244 ("release_group", Field::ReleaseGroup),
245 ("genre", Field::Genre),
246 ("label", Field::Label),
247 ("comment", Field::Comment),
248 ("lyrics", Field::Lyrics),
249 ("path", Field::Path),
250 ("codec", Field::Codec),
251 ("format", Field::Codec),
252 ("container", Field::Container),
253 ("year", Field::Year),
254 ("duration", Field::Duration),
255 ("length", Field::Duration),
256 ("size", Field::Size),
257 ("bitrate", Field::Bitrate),
258 ("samplerate", Field::SampleRate),
259 ("bitdepth", Field::BitDepth),
260 ("channels", Field::Channels),
261 ("ref", Field::Reference),
262 ("lossless", Field::Lossless),
263 ("compilation", Field::Compilation),
264 ("played", Field::Played),
265 ("rating", Field::Rating(Scope::Track)),
266 ("album.rating", Field::Rating(Scope::Album)),
267 ("artist.rating", Field::Rating(Scope::Artist)),
268 ("loved", Field::LovedAnywhere),
269 ("track.loved", Field::Loved(Scope::Track)),
270 ("album.loved", Field::Loved(Scope::Album)),
271 ("artist.loved", Field::Loved(Scope::Artist)),
272 ("tag", Field::Tag(Scope::Track)),
273 ("album.tag", Field::Tag(Scope::Album)),
274 ("artist.tag", Field::Tag(Scope::Artist)),
275 ("composer", Field::Credit("composer")),
280 ("lyricist", Field::Credit("lyricist")),
281 ("producer", Field::Credit("producer")),
282 ("engineer", Field::Credit("engineer")),
283 ("performer", Field::Credit("performer")),
284 ("conductor", Field::Credit("conductor")),
285 ("orchestra", Field::Credit("orchestra")),
286 ("choir", Field::Credit("choir")),
287 ("ensemble", Field::Credit("ensemble")),
288 ("soloist", Field::Credit("soloist")),
289 ("remixer", Field::Credit("remixer")),
290 ("featured", Field::Credit("featured")),
291 ("mainartist", Field::Credit("main")),
292 ("performing", Field::Performing),
293 ("instrument", Field::Instrument),
294 ("guest", Field::Guest),
295 ("compilationartist", Field::CompilationArtist),
296 ("compilation-artist", Field::CompilationArtist),
297 ("contributor", Field::Contributor),
298 ("collaborator", Field::Collaborator),
299 ("with", Field::Collaborator),
300 ("note", Field::Note(Scope::Track)),
301 ("album.note", Field::Note(Scope::Album)),
302 ("artist.note", Field::Note(Scope::Artist)),
303];
304
305fn field_named(name: &str) -> Option<Field> {
306 let wanted = name.trim().to_lowercase();
307 FIELD_NAMES
308 .iter()
309 .find(|(n, _)| *n == wanted)
310 .map(|(_, f)| f.clone())
311}
312
313fn is_numeric(field: &Field) -> bool {
314 matches!(
315 field,
316 Field::Year
317 | Field::Duration
318 | Field::Size
319 | Field::Bitrate
320 | Field::SampleRate
321 | Field::BitDepth
322 | Field::Channels
323 | Field::Played
324 | Field::Rating(_)
325 )
326}
327
328fn is_flag(field: &Field) -> bool {
329 matches!(
330 field,
331 Field::Lossless | Field::Compilation | Field::Loved(_) | Field::LovedAnywhere
332 )
333}
334
335pub fn rescoped(query: &Query, scope: Scope) -> Query {
354 match query {
355 Query::Browse(filter) => Query::Browse(filter.clone()),
356 Query::All => Query::All,
357 Query::And(parts) => Query::And(parts.iter().map(|p| rescoped(p, scope)).collect()),
358 Query::Or(parts) => Query::Or(parts.iter().map(|p| rescoped(p, scope)).collect()),
359 Query::Not(inner) => Query::Not(Box::new(rescoped(inner, scope))),
360 Query::Term(term) => Query::Term(Term {
361 field: match &term.field {
362 Field::Rating(_) => Field::Rating(scope),
363 Field::Loved(_) => Field::Loved(scope),
364 Field::Tag(_) => Field::Tag(scope),
365 Field::Note(_) => Field::Note(scope),
366 other => other.clone(),
367 },
368 test: term.test.clone(),
369 }),
370 }
371}
372
373pub fn asks_about_the_track_itself(query: &Query) -> bool {
376 match query {
377 Query::Browse(_) => false,
378 Query::All => false,
379 Query::And(parts) | Query::Or(parts) => parts.iter().any(asks_about_the_track_itself),
380 Query::Not(inner) => asks_about_the_track_itself(inner),
381 Query::Term(term) => matches!(
382 term.field,
383 Field::Rating(Scope::Track)
384 | Field::Loved(Scope::Track)
385 | Field::Tag(Scope::Track)
386 | Field::Note(Scope::Track)
387 ),
388 }
389}
390
391fn asks_whether_it_holds_anything(field: &Field) -> bool {
413 is_flag(field) || matches!(field, Field::Rating(_) | Field::Note(_) | Field::Tag(_))
414}
415
416fn closed_vocabulary(field: &Field) -> Option<&'static str> {
424 Some(match field {
425 Field::Genre => "genre",
426 Field::Label => "label",
427 Field::Credit(_)
428 | Field::Performing
429 | Field::Guest
430 | Field::CompilationArtist
431 | Field::Contributor
432 | Field::Collaborator => "artist",
433 _ => return None,
434 })
435}
436
437pub fn unknown_values(query: &Query, context: &Context) -> Vec<(String, String)> {
442 let mut found = Vec::new();
443 collect_unknown(query, context, &mut found);
444 found
445}
446
447fn collect_unknown(query: &Query, context: &Context, out: &mut Vec<(String, String)>) {
448 match query {
449 Query::Browse(_) | Query::All => {}
450 Query::And(parts) | Query::Or(parts) => {
451 for part in parts {
452 collect_unknown(part, context, out);
453 }
454 }
455 Query::Not(inner) => collect_unknown(inner, context, out),
456 Query::Term(term) => {
457 let Some(what) = closed_vocabulary(&term.field) else {
458 return;
459 };
460 let wanted = match &term.test {
461 Test::Contains(value) | Test::Is(value) => text::normalize(value),
462 _ => return,
463 };
464 if wanted.is_empty() {
465 return;
466 }
467 let known = match &term.field {
468 Field::Genre => context
469 .catalog
470 .genres
471 .iter()
472 .any(|g| g.key.contains(&wanted)),
473 Field::Label => context.catalog.labels.iter().any(|l| {
474 l.key.contains(&wanted)
475 || l.mbid
476 .as_deref()
477 .is_some_and(|mbid| text::normalize(mbid).contains(&wanted))
478 || context
479 .sourced
480 .label_mbids
481 .get(&l.id)
482 .is_some_and(|mbid| text::normalize(mbid).contains(&wanted))
483 }),
484 Field::Credit(_)
488 | Field::Performing
489 | Field::Guest
490 | Field::CompilationArtist
491 | Field::Contributor
492 | Field::Collaborator => {
493 context.catalog.artists.iter().any(|a| {
494 a.key.contains(&wanted)
495 || a.aliases
496 .iter()
497 .any(|alias| text::normalize(alias).contains(&wanted))
498 || a.mbid
499 .as_deref()
500 .is_some_and(|mbid| text::normalize(mbid) == wanted)
501 }) || context
502 .sourced
503 .credits
504 .values()
505 .chain(context.sourced.edition_credits.values())
506 .flatten()
507 .any(|credit| {
508 text::normalize(&credit.artist_name).contains(&wanted)
509 || text::normalize(&credit.artist_mbid) == wanted
510 || credit
511 .credited_as
512 .as_deref()
513 .is_some_and(|name| text::normalize(name).contains(&wanted))
514 })
515 }
516 _ => true,
517 };
518 if !known {
519 out.push((what.to_string(), wanted));
520 }
521 }
522 }
523}
524
525pub fn field_names() -> Vec<&'static str> {
527 FIELD_NAMES.iter().map(|(name, _)| *name).collect()
528}
529
530pub mod browse;
535mod indexes;
536mod sorting;
537pub use sorting::{Sort, SortKey, sort, sort_key_names};
538
539pub struct Context<'a> {
541 pub catalog: &'a Catalog,
543 pub data: &'a UserData,
545 pub owner: &'a str,
547 sourced: SourcedRelations,
549 browse_claims: browse::Claims,
550 local: indexes::LocalIndexes<'a>,
551}
552
553impl<'a> Context<'a> {
554 pub fn new(catalog: &'a Catalog, data: &'a UserData, owner: &'a str) -> Context<'a> {
556 Context {
557 catalog,
558 data,
559 owner,
560 sourced: SourcedRelations::default(),
561 browse_claims: browse::Claims::default(),
562 local: indexes::LocalIndexes::new(catalog, data, owner),
563 }
564 }
565
566 pub fn with_sources(mut self, sources: &crate::sources::Sources) -> Context<'a> {
570 self.sourced = SourcedRelations::new(self.catalog, sources);
571 self.browse_claims = browse::Claims::new(self.catalog, sources);
572 self
573 }
574}
575
576#[derive(Default)]
577struct SourcedRelations {
578 works: BTreeMap<Id, Vec<crate::sources::WorkLink>>,
579 work_parents: BTreeMap<Id, Vec<crate::sources::WorkParentLink>>,
580 credits: BTreeMap<Id, Vec<crate::sources::CreditLink>>,
581 edition_credits: BTreeMap<Id, Vec<crate::sources::CreditLink>>,
582 label_mbids: BTreeMap<Id, String>,
583}
584
585impl SourcedRelations {
586 fn new(catalog: &Catalog, sources: &crate::sources::Sources) -> SourcedRelations {
587 let mut indexed = SourcedRelations::default();
588 for link in sources
589 .work_links(catalog)
590 .into_iter()
591 .filter(|link| link.trusted)
592 {
593 let works = indexed.works.entry(link.recording_id).or_default();
594 if !works.contains(&link.work) {
595 works.push(link.work);
596 }
597 }
598 for link in sources
599 .work_parent_links(catalog)
600 .into_iter()
601 .filter(|link| link.trusted)
602 {
603 for &recording_id in link.applicable_recording_ids.iter() {
604 let parents = indexed.work_parents.entry(recording_id).or_default();
605 if !parents.contains(&link.parent) {
606 parents.push(link.parent.clone());
607 }
608 }
609 }
610 for link in sources
611 .credit_links(catalog)
612 .into_iter()
613 .filter(|link| link.trusted)
614 {
615 for &recording_id in link.applicable_recording_ids.iter() {
616 let credits = indexed.credits.entry(recording_id).or_default();
617 if !credits.contains(&link.credit) {
618 credits.push(link.credit.clone());
619 }
620 }
621 }
622 for link in sources
623 .edition_credit_links(catalog)
624 .into_iter()
625 .filter(|link| link.trusted)
626 {
627 let credits = indexed.edition_credits.entry(link.release_id).or_default();
628 if !credits.contains(&link.credit) {
629 credits.push(link.credit);
630 }
631 }
632 for label in &catalog.labels {
633 let Some(identity) = sources.label_identity(catalog, label.id) else {
634 continue;
635 };
636 let mbid = match identity {
637 crate::sources::LabelIdentityResolution::Confirmed { mbid, .. }
638 | crate::sources::LabelIdentityResolution::Agrees { mbid, .. }
639 | crate::sources::LabelIdentityResolution::Accepted { mbid, .. } => mbid,
640 _ => continue,
641 };
642 indexed.label_mbids.insert(label.id, mbid);
643 }
644 indexed
645 }
646}
647
648pub fn run(query: &Query, context: &Context) -> Vec<Id> {
653 if let Query::Browse(filter) = query {
654 return browse::tracks(filter, context);
655 }
656 context
657 .catalog
658 .tracks
659 .iter()
660 .filter(|track| matches(query, context, track.id))
661 .map(|track| track.id)
662 .collect()
663}
664
665pub fn matches(query: &Query, context: &Context, track: Id) -> bool {
667 match query {
668 Query::Browse(filter) => browse::tracks(filter, context).contains(&track),
669 Query::All => true,
670 Query::And(parts) => parts.iter().all(|p| matches(p, context, track)),
671 Query::Or(parts) => parts.iter().any(|p| matches(p, context, track)),
672 Query::Not(inner) => !matches(inner, context, track),
673 Query::Term(term) => term_matches(term, context, track),
674 }
675}
676
677fn term_matches(term: &Term, context: &Context, track: Id) -> bool {
678 match &term.test {
679 Test::Set => flag_of(&term.field, context, track),
680 Test::Unset => !flag_of(&term.field, context, track),
681 Test::Compare(compare, wanted) => match number_of(&term.field, context, track) {
682 None => false,
686 Some(value) => match compare {
687 Compare::Equal => (value - wanted).abs() < f64::EPSILON,
688 Compare::Greater => value > *wanted,
689 Compare::AtLeast => value >= *wanted,
690 Compare::Less => value < *wanted,
691 Compare::AtMost => value <= *wanted,
692 },
693 },
694 Test::Between(low, high) => match number_of(&term.field, context, track) {
695 None => false,
696 Some(value) => {
697 low.map(|l| value >= l).unwrap_or(true) && high.map(|h| value <= h).unwrap_or(true)
698 }
699 },
700 Test::Contains(wanted) => {
701 let wanted = text::normalize(wanted);
702 texts_of(&term.field, context, track)
703 .iter()
704 .any(|value| text::normalize(value).contains(&wanted))
705 }
706 Test::Is(wanted) => {
707 let wanted = text::normalize(wanted);
708 texts_of(&term.field, context, track)
709 .iter()
710 .any(|value| text::normalize(value) == wanted)
711 }
712 }
713}
714
715fn scoped(scope: Scope, context: &Context, track: Id) -> Option<EntityRef> {
717 let catalog = context.catalog;
718 match scope {
719 Scope::Track => EntityRef::of(catalog, EntityKind::Track, track),
720 Scope::Album => catalog
721 .track(track)
722 .and_then(|t| t.release_id)
723 .and_then(|r| EntityRef::of(catalog, EntityKind::Release, r)),
724 Scope::Artist => main_artist(context, track)
725 .and_then(|artist| EntityRef::of(catalog, EntityKind::Artist, artist.id)),
726 }
727}
728
729fn annotation<'a>(
730 scope: Scope,
731 context: &'a Context,
732 track: Id,
733) -> Option<&'a crate::user::Annotation> {
734 let reference = scoped(scope, context, track)?;
735 context
736 .local
737 .annotation(context.data, context.owner, &reference)
738}
739
740fn main_artist<'a>(context: &Context<'a>, track: Id) -> Option<&'a crate::model::Artist> {
741 context
742 .local
743 .credits(context.catalog, EntityKind::Track, track)
744 .iter()
745 .filter(|credit| credit.role == "main")
746 .find_map(|credit| context.catalog.artist(credit.artist_id))
747}
748
749fn local_credits<'a>(context: &Context<'a>, track: Id) -> Vec<&'a crate::model::Credit> {
750 let mut credits = context
751 .local
752 .credits(context.catalog, EntityKind::Track, track)
753 .to_vec();
754 if let Some(release) = context.catalog.track(track).and_then(|row| row.release_id) {
755 credits.extend_from_slice(&context.local.credits(
756 context.catalog,
757 EntityKind::Release,
758 release,
759 ));
760 }
761 credits
762}
763
764fn flag_of(field: &Field, context: &Context, track: Id) -> bool {
765 match field {
766 Field::Lossless => context
767 .catalog
768 .track(track)
769 .and_then(|t| context.catalog.file(t.file_id))
770 .map(|f| f.properties.lossless)
771 .unwrap_or(false),
772 Field::Compilation => context
773 .catalog
774 .track(track)
775 .and_then(|t| t.release_id)
776 .and_then(|r| context.catalog.release(r))
777 .map(|r| r.is_compilation)
778 .unwrap_or(false),
779 Field::Loved(scope) => annotation(*scope, context, track)
780 .map(|a| a.loved)
781 .unwrap_or(false),
782 Field::LovedAnywhere => [Scope::Track, Scope::Album, Scope::Artist]
783 .into_iter()
784 .any(|scope| annotation(scope, context, track).is_some_and(|a| a.loved)),
785 _ => {
787 !texts_of(field, context, track).is_empty()
788 || number_of(field, context, track).is_some()
789 }
790 }
791}
792
793fn number_of(field: &Field, context: &Context, track: Id) -> Option<f64> {
794 let catalog = context.catalog;
795 let track_row = catalog.track(track)?;
796 let file = catalog.file(track_row.file_id);
797 match field {
798 Field::Year => track_row
799 .release_id
800 .and_then(|r| catalog.release(r))
801 .and_then(|r| r.year)
802 .map(f64::from),
803 Field::Duration => track_row.duration_ms.map(|d| d as f64),
804 Field::Size => file.map(|f| f.size as f64),
805 Field::Bitrate => file.and_then(|f| f.properties.bitrate_kbps).map(f64::from),
806 Field::SampleRate => file.and_then(|f| f.properties.sample_rate).map(f64::from),
807 Field::BitDepth => file.and_then(|f| f.properties.bit_depth).map(f64::from),
808 Field::Channels => file.and_then(|f| f.properties.channels).map(f64::from),
809 Field::Played => {
810 let reference = scoped(Scope::Track, context, track)?;
811 Some(f64::from(context.local.play_count(
812 context.data,
813 context.owner,
814 &reference,
815 )))
816 }
817 Field::Rating(scope) => annotation(*scope, context, track)
818 .and_then(|a| a.rating)
819 .map(f64::from),
820 _ => None,
821 }
822}
823
824fn texts_of(field: &Field, context: &Context, track: Id) -> Vec<String> {
825 let catalog = context.catalog;
826 let Some(track_row) = catalog.track(track) else {
827 return Vec::new();
828 };
829 let file = catalog.file(track_row.file_id);
830 let release = track_row.release_id.and_then(|r| catalog.release(r));
831 match field {
832 Field::Title => vec![track_row.title.clone()],
833 Field::Artist => {
834 let mut values = Vec::new();
835 for credit in context
836 .local
837 .credits(context.catalog, EntityKind::Track, track)
838 .iter()
839 {
840 extend_artist_values(catalog, credit.artist_id, &mut values);
841 if let Some(credited_as) = &credit.credited_as {
842 push_unique(&mut values, credited_as.clone());
843 }
844 }
845 if let Some(release_id) = track_row.release_id {
846 for credit in context
847 .local
848 .credits(context.catalog, EntityKind::Release, release_id)
849 .iter()
850 {
851 extend_artist_values(catalog, credit.artist_id, &mut values);
852 }
853 }
854 for credit in sourced_credits(context, track) {
855 extend_sourced_credit_values(credit, &mut values);
856 }
857 values
858 }
859 Field::Album => release
860 .map(|r| {
861 let mut values = vec![r.title.clone()];
862 if let Some(mbid) = &r.mbid {
863 values.push(mbid.clone());
864 }
865 values
866 })
867 .unwrap_or_default(),
868 Field::Recording => catalog
869 .recording(track_row.recording_id)
870 .map(|recording| {
871 let mut values = vec![recording.title.clone()];
872 if let Some(mbid) = &recording.mbid {
873 values.push(mbid.clone());
874 }
875 values
876 })
877 .unwrap_or_default(),
878 Field::Work => {
879 let mut values = catalog
880 .recording(track_row.recording_id)
881 .map(|recording| {
882 recording
883 .work_ids
884 .iter()
885 .filter_map(|&id| catalog.work(id))
886 .flat_map(|work| vec![work.title.clone(), work.mbid.clone()])
887 .collect()
888 })
889 .unwrap_or_default();
890 if let Some(work_tag) = catalog
893 .file(track_row.file_id)
894 .and_then(|file| file.first_tag("grouping"))
895 {
896 push_unique(&mut values, work_tag.to_string());
897 }
898 if let Some(works) = context.sourced.works.get(&track_row.recording_id) {
899 for work in works {
900 push_unique(&mut values, work.title.clone());
901 push_unique(&mut values, work.mbid.clone());
902 }
903 }
904 if let Some(parents) = context.sourced.work_parents.get(&track_row.recording_id) {
905 for parent in parents {
906 push_unique(&mut values, parent.title.clone());
907 push_unique(&mut values, parent.mbid.clone());
908 }
909 }
910 values
911 }
912 Field::Movement => catalog
913 .file(track_row.file_id)
914 .map(|file| {
915 ["movement", "movementnumber"]
916 .into_iter()
917 .filter_map(|tag| file.first_tag(tag).map(str::to_string))
918 .collect()
919 })
920 .unwrap_or_default(),
921 Field::ReleaseGroup => release
922 .and_then(|r| r.release_group_id)
923 .and_then(|id| catalog.release_group(id))
924 .map(|group| vec![group.title.clone(), group.mbid.clone()])
925 .unwrap_or_default(),
926 Field::AlbumArtist => release
927 .and_then(|r| r.album_artist_id)
928 .map(|a| artist_values(catalog, a))
929 .unwrap_or_default(),
930 Field::Genre => {
931 let mut names: Vec<String> = context
932 .local
933 .genres(context.catalog, EntityKind::Track, track)
934 .iter()
935 .map(|g| g.name.clone())
936 .collect();
937 if let Some(release) = release {
938 names.extend(
939 context
940 .local
941 .genres(context.catalog, EntityKind::Release, release.id)
942 .iter()
943 .map(|g| g.name.clone()),
944 );
945 }
946 names
947 }
948 Field::Label => release
949 .map(|r| {
950 r.label_ids.iter().fold(Vec::new(), |mut values, &id| {
951 if let Some(label) = catalog.label(id) {
952 values.push(label.name.clone());
953 if let Some(mbid) = &label.mbid {
954 values.push(mbid.clone());
955 }
956 if let Some(mbid) = context.sourced.label_mbids.get(&id) {
957 push_unique(&mut values, mbid.clone());
958 }
959 }
960 values
961 })
962 })
963 .unwrap_or_default(),
964 Field::Comment => file
965 .and_then(|f| f.tags.get("comment"))
966 .cloned()
967 .unwrap_or_default(),
968 Field::Lyrics => catalog
973 .lyrics_of_track(track)
974 .map(|lyrics| vec![lyrics.text()])
975 .unwrap_or_default(),
976 Field::Reference => EntityRef::of(catalog, EntityKind::Track, track)
977 .map(|r| vec![r.to_token()])
978 .unwrap_or_default(),
979 Field::Container => file
980 .map(|f| vec![f.properties.container.clone()])
981 .unwrap_or_default(),
982 Field::Path => file.map(|f| vec![f.path.clone()]).unwrap_or_default(),
983 Field::Codec => file
984 .map(|f| {
985 vec![
986 f.properties.codec.clone(),
987 f.properties.container.clone(),
988 f.properties.quality_label(),
989 ]
990 })
991 .unwrap_or_default(),
992 Field::Performing => credits_as_text(context, track, |role, _| {
993 crate::model::is_performing_role(role)
994 }),
995 Field::Credit(role) => credits_as_text(context, track, |credited, attributes| {
996 credited == *role
997 || (*role == "orchestra" && credited == "performing orchestra")
998 || (*role == "soloist"
999 && matches!(credited, "performer" | "instrument" | "vocal")
1000 && attributes.iter().any(|attribute| attribute.name == "solo"))
1001 }),
1002 Field::Instrument => {
1003 let mut values = Vec::new();
1004 for credit in context
1005 .local
1006 .credits(context.catalog, EntityKind::Track, track)
1007 .iter()
1008 {
1009 for attribute in &credit.attributes {
1010 push_unique(&mut values, attribute.name.clone());
1011 if let Some(value) = &attribute.value {
1012 push_unique(&mut values, value.clone());
1013 }
1014 if let Some(credited_as) = &attribute.credited_as {
1015 push_unique(&mut values, credited_as.clone());
1016 }
1017 }
1018 }
1019 for credit in sourced_credits(context, track) {
1020 for attribute in &credit.attributes {
1021 push_unique(&mut values, attribute.name.clone());
1022 if let Some(value) = &attribute.value {
1023 push_unique(&mut values, value.clone());
1024 }
1025 if let Some(credited_as) = &attribute.credited_as {
1026 push_unique(&mut values, credited_as.clone());
1027 }
1028 }
1029 }
1030 values
1031 }
1032 Field::Guest | Field::CompilationArtist | Field::Contributor | Field::Collaborator => {
1033 participation_values(field, context, track, release)
1034 }
1035 Field::Tag(scope) => annotation(*scope, context, track)
1036 .map(|a| a.tags.iter().cloned().collect())
1037 .unwrap_or_default(),
1038 Field::Note(scope) => annotation(*scope, context, track)
1039 .and_then(|a| a.note.clone())
1040 .map(|n| vec![n])
1041 .unwrap_or_default(),
1042 Field::Anything => {
1044 let mut all = vec![track_row.title.clone()];
1045 all.extend(texts_of(&Field::Artist, context, track));
1046 all.extend(texts_of(&Field::Album, context, track));
1047 all
1048 }
1049 _ => Vec::new(),
1050 }
1051}
1052
1053fn push_unique(values: &mut Vec<String>, value: String) {
1054 if !values.iter().any(|existing| existing == &value) {
1055 values.push(value);
1056 }
1057}
1058
1059fn artist_values(catalog: &Catalog, artist_id: Id) -> Vec<String> {
1060 let Some(artist) = catalog.artist(artist_id) else {
1061 return Vec::new();
1062 };
1063 let mut values = vec![artist.name.clone()];
1064 if let Some(mbid) = &artist.mbid {
1065 values.push(mbid.clone());
1066 }
1067 values.extend(artist.aliases.iter().cloned());
1068 values
1069}
1070
1071fn extend_artist_values(catalog: &Catalog, artist_id: Id, values: &mut Vec<String>) {
1072 for value in artist_values(catalog, artist_id) {
1073 push_unique(values, value);
1074 }
1075}
1076
1077fn sourced_credits<'a>(
1078 context: &'a Context<'_>,
1079 track_id: Id,
1080) -> Vec<&'a crate::sources::CreditLink> {
1081 let Some(track) = context.catalog.track(track_id) else {
1082 return Vec::new();
1083 };
1084 context
1085 .sourced
1086 .credits
1087 .get(&track.recording_id)
1088 .into_iter()
1089 .flat_map(|credits| credits.iter())
1090 .chain(
1091 track
1092 .release_id
1093 .and_then(|id| context.sourced.edition_credits.get(&id))
1094 .into_iter()
1095 .flat_map(|credits| credits.iter()),
1096 )
1097 .collect()
1098}
1099
1100fn extend_sourced_credit_values(credit: &crate::sources::CreditLink, values: &mut Vec<String>) {
1101 push_unique(values, credit.artist_name.clone());
1102 push_unique(values, credit.artist_mbid.clone());
1103 if let Some(credited_as) = &credit.credited_as {
1104 push_unique(values, credited_as.clone());
1105 }
1106}
1107
1108fn credits_as_text(
1109 context: &Context,
1110 track: Id,
1111 wanted: impl Fn(&str, &[crate::model::CreditAttribute]) -> bool,
1112) -> Vec<String> {
1113 let catalog = context.catalog;
1114 let mut values = Vec::new();
1115 for credit in local_credits(context, track)
1116 .into_iter()
1117 .filter(|credit| wanted(&credit.role, &credit.attributes))
1118 {
1119 extend_artist_values(catalog, credit.artist_id, &mut values);
1120 if let Some(credited_as) = &credit.credited_as {
1121 push_unique(&mut values, credited_as.clone());
1122 }
1123 }
1124 for credit in sourced_credits(context, track)
1125 .into_iter()
1126 .filter(|credit| wanted(&credit.role, &credit.attributes))
1127 {
1128 extend_sourced_credit_values(credit, &mut values);
1129 }
1130 values
1131}
1132
1133fn participation_values(
1134 field: &Field,
1135 context: &Context,
1136 track: Id,
1137 release: Option<&crate::model::Release>,
1138) -> Vec<String> {
1139 let catalog = context.catalog;
1140 if catalog.track(track).is_none() {
1141 return Vec::new();
1142 }
1143 let performing: Vec<&crate::model::Credit> = context
1144 .local
1145 .credits(context.catalog, EntityKind::Track, track)
1146 .iter()
1147 .copied()
1148 .filter(|credit| crate::model::is_performing_role(&credit.role))
1149 .collect();
1150 let sourced_performing: Vec<&crate::sources::CreditLink> = sourced_credits(context, track)
1151 .into_iter()
1152 .filter(|credit| crate::model::is_performing_role(&credit.role))
1153 .collect();
1154 let mut distinct_performers: BTreeSet<String> = performing
1155 .iter()
1156 .map(|credit| local_artist_key(catalog, credit.artist_id))
1157 .collect();
1158 distinct_performers.extend(
1159 sourced_performing
1160 .iter()
1161 .map(|credit| sourced_artist_key(catalog, credit)),
1162 );
1163 if matches!(field, Field::Contributor) {
1164 let mut values = Vec::new();
1165 for credit in local_credits(context, track)
1166 .into_iter()
1167 .filter(|credit| !crate::model::is_performing_role(&credit.role))
1168 {
1169 extend_artist_values(catalog, credit.artist_id, &mut values);
1170 }
1171 for credit in sourced_credits(context, track)
1172 .into_iter()
1173 .filter(|credit| !crate::model::is_performing_role(&credit.role))
1174 {
1175 extend_sourced_credit_values(credit, &mut values);
1176 }
1177 return values;
1178 }
1179 let Some(release) = release else {
1180 return if matches!(field, Field::Collaborator) && distinct_performers.len() > 1 {
1181 performer_values(catalog, &performing, &sourced_performing)
1182 } else {
1183 Vec::new()
1184 };
1185 };
1186 let mut values = Vec::new();
1187 match field {
1188 Field::Guest => {
1189 if !release.is_compilation {
1190 for credit in performing {
1191 if release.album_artist_id != Some(credit.artist_id) {
1192 extend_artist_values(catalog, credit.artist_id, &mut values);
1193 }
1194 }
1195 for credit in sourced_performing {
1196 if !is_album_artist(catalog, release, credit) {
1197 extend_sourced_credit_values(credit, &mut values);
1198 }
1199 }
1200 }
1201 }
1202 Field::CompilationArtist => {
1203 if release.is_compilation {
1204 for credit in performing {
1205 if release.album_artist_id != Some(credit.artist_id) {
1206 extend_artist_values(catalog, credit.artist_id, &mut values);
1207 }
1208 }
1209 for credit in sourced_performing {
1210 if !is_album_artist(catalog, release, credit) {
1211 extend_sourced_credit_values(credit, &mut values);
1212 }
1213 }
1214 }
1215 }
1216 Field::Contributor => unreachable!("contributors are handled before release lookup"),
1217 Field::Collaborator if distinct_performers.len() > 1 => {
1218 return performer_values(catalog, &performing, &sourced_performing);
1219 }
1220 _ => {}
1221 }
1222 values
1223}
1224
1225fn local_artist_key(catalog: &Catalog, artist_id: Id) -> String {
1226 catalog
1227 .artist(artist_id)
1228 .map(|artist| {
1229 artist
1230 .mbid
1231 .clone()
1232 .unwrap_or_else(|| format!("name:{}", artist.key))
1233 })
1234 .unwrap_or_else(|| format!("local:{artist_id}"))
1235}
1236
1237fn sourced_artist_key(catalog: &Catalog, credit: &crate::sources::CreditLink) -> String {
1238 if !credit.artist_mbid.trim().is_empty() {
1239 return credit.artist_mbid.clone();
1240 }
1241 let credited_key = credit.credited_as.as_deref().map(text::normalize);
1242 if let Some(artist) = catalog.artists.iter().find(|artist| {
1243 artist.key == text::normalize(&credit.artist_name)
1244 || credited_key.as_deref() == Some(artist.key.as_str())
1245 }) {
1246 return local_artist_key(catalog, artist.id);
1247 }
1248 format!("name:{}", text::normalize(&credit.artist_name))
1249}
1250
1251fn is_album_artist(
1252 catalog: &Catalog,
1253 release: &crate::model::Release,
1254 credit: &crate::sources::CreditLink,
1255) -> bool {
1256 let Some(artist) = release.album_artist_id.and_then(|id| catalog.artist(id)) else {
1257 return false;
1258 };
1259 local_artist_key(catalog, artist.id) == sourced_artist_key(catalog, credit)
1260}
1261
1262fn performer_values(
1263 catalog: &Catalog,
1264 local: &[&crate::model::Credit],
1265 sourced: &[&crate::sources::CreditLink],
1266) -> Vec<String> {
1267 let mut values = Vec::new();
1268 let mut seen = BTreeSet::new();
1269 for credit in local {
1270 if seen.insert(credit.artist_id) {
1271 extend_artist_values(catalog, credit.artist_id, &mut values);
1272 }
1273 }
1274 for credit in sourced {
1275 extend_sourced_credit_values(credit, &mut values);
1276 }
1277 values
1278}
1279
1280#[cfg(test)]
1281#[path = "query_tests.rs"]
1282mod tests;