aede_core/
contributors.rs1use std::collections::{BTreeMap, BTreeSet};
8
9use crate::model::{Catalog, Id};
10use crate::sources::Sources;
11use crate::text;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct SourcedContributor {
16 pub mbid: String,
18 pub name: String,
20 pub names: Vec<String>,
22 pub recording_ids: Vec<Id>,
24 pub release_ids: Vec<Id>,
26 pub work_mbids: Vec<String>,
28}
29
30struct Builder {
31 name: String,
32 fetched_at: u64,
33 names: BTreeSet<String>,
34 recording_ids: BTreeSet<Id>,
35 release_ids: BTreeSet<Id>,
36 work_mbids: BTreeSet<String>,
37}
38
39pub fn sourced(catalog: &Catalog, sources: &Sources) -> Vec<SourcedContributor> {
41 let local_mbids: BTreeSet<&str> = catalog
42 .artists
43 .iter()
44 .filter_map(|artist| artist.mbid.as_deref())
45 .collect();
46 let mut grouped: BTreeMap<String, Builder> = BTreeMap::new();
47 for link in sources
48 .credit_links(catalog)
49 .into_iter()
50 .filter(|link| link.trusted)
51 {
52 let mbid = link.credit.artist_mbid.trim();
53 if mbid.is_empty() || local_mbids.contains(mbid) {
54 continue;
55 }
56 let name = link.credit.artist_name.trim();
57 let builder = grouped.entry(mbid.to_string()).or_insert_with(|| Builder {
58 name: name.to_string(),
59 fetched_at: link.fetched_at,
60 names: BTreeSet::new(),
61 recording_ids: BTreeSet::new(),
62 release_ids: BTreeSet::new(),
63 work_mbids: BTreeSet::new(),
64 });
65 if !name.is_empty() {
66 builder.names.insert(name.to_string());
67 if builder.name.is_empty()
68 || link.fetched_at > builder.fetched_at
69 || (link.fetched_at == builder.fetched_at && name < builder.name.as_str())
70 {
71 builder.name = name.to_string();
72 builder.fetched_at = link.fetched_at;
73 }
74 }
75 if let Some(credited_as) = link.credit.credited_as.as_deref()
76 && !credited_as.trim().is_empty()
77 {
78 builder.names.insert(credited_as.trim().to_string());
79 }
80 builder
81 .recording_ids
82 .extend(link.applicable_recording_ids.iter().copied());
83 if let Some(work) = &link.work {
84 builder.work_mbids.insert(work.mbid.clone());
85 }
86 }
87 for link in sources
88 .edition_credit_links(catalog)
89 .into_iter()
90 .filter(|link| link.trusted)
91 {
92 let mbid = link.credit.artist_mbid.trim();
93 if mbid.is_empty() || local_mbids.contains(mbid) {
94 continue;
95 }
96 let name = link.credit.artist_name.trim();
97 let builder = grouped.entry(mbid.to_string()).or_insert_with(|| Builder {
98 name: name.to_string(),
99 fetched_at: link.fetched_at,
100 names: BTreeSet::new(),
101 recording_ids: BTreeSet::new(),
102 release_ids: BTreeSet::new(),
103 work_mbids: BTreeSet::new(),
104 });
105 if !name.is_empty() {
106 builder.names.insert(name.to_string());
107 if builder.name.is_empty()
108 || link.fetched_at > builder.fetched_at
109 || (link.fetched_at == builder.fetched_at && name < builder.name.as_str())
110 {
111 builder.name = name.to_string();
112 builder.fetched_at = link.fetched_at;
113 }
114 }
115 if let Some(credited_as) = link.credit.credited_as.as_deref()
116 && !credited_as.trim().is_empty()
117 {
118 builder.names.insert(credited_as.trim().to_string());
119 }
120 builder.release_ids.insert(link.release_id);
121 }
122 grouped
123 .into_iter()
124 .map(|(mbid, builder)| SourcedContributor {
125 name: if builder.name.is_empty() {
126 mbid.clone()
127 } else {
128 builder.name
129 },
130 mbid,
131 names: builder.names.into_iter().collect(),
132 recording_ids: builder.recording_ids.into_iter().collect(),
133 release_ids: builder.release_ids.into_iter().collect(),
134 work_mbids: builder.work_mbids.into_iter().collect(),
135 })
136 .collect()
137}
138
139pub fn matches_name(contributor: &SourcedContributor, query: &str, exact: bool) -> bool {
142 let wanted = text::normalize(query);
143 !wanted.is_empty()
144 && contributor.names.iter().any(|name| {
145 let key = text::normalize(name);
146 if exact {
147 key == wanted
148 } else {
149 key.contains(&wanted)
150 }
151 })
152}
153
154#[cfg(test)]
155#[path = "contributors_tests.rs"]
156mod tests;