1use super::{Context, QueryError, error};
3use crate::model::{Catalog, EntityKind, Id};
4use crate::sources::{Facts, Sources};
5use crate::user::EntityRef;
6use serde_json::Value;
7use std::collections::{BTreeMap, BTreeSet};
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct Filter {
12 view: String,
13 reverse: bool,
14 mode: String,
15 years: [u64; 2],
16 alphabet: [u64; 2],
17 unknown: bool,
18 query: String,
19 countries: Vec<Option<String>>,
20 genres: Vec<String>,
21 ratings: Option<[u64; 2]>,
22 tags: Vec<String>,
23 label: Option<EntityRef>,
24 composers: Vec<EntityRef>,
25}
26pub fn parse(input: &str) -> Result<Filter, QueryError> {
28 if input.len() > 8192 {
29 return Err(error("browse filter exceeds 8192 bytes"));
30 }
31 let v: Value = serde_json::from_str(input).map_err(|_| error("invalid browse filter JSON"))?;
32 let o = v
33 .as_object()
34 .ok_or_else(|| error("browse filter must be an object"))?;
35 if o.keys().any(|k| {
36 ![
37 "view",
38 "reverse",
39 "mode",
40 "years",
41 "alphabet",
42 "includeUnknownYear",
43 "query",
44 "countries",
45 "genre",
46 "genres",
47 "ratings",
48 "tags",
49 "label",
50 "composers",
51 ]
52 .contains(&k.as_str())
53 }) {
54 return Err(error("unknown browse filter field"));
55 }
56 let string = |key: &str| {
57 v[key]
58 .as_str()
59 .map(str::to_owned)
60 .ok_or_else(|| error(format!("invalid browse {key}")))
61 };
62 let interval = |key: &str, max: u64| -> Result<[u64; 2], QueryError> {
63 let values = v[key]
64 .as_array()
65 .filter(|a| a.len() == 2)
66 .ok_or_else(|| error("invalid browse interval"))?;
67 let low = values[0]
68 .as_u64()
69 .ok_or_else(|| error("invalid browse bound"))?;
70 let high = values[1]
71 .as_u64()
72 .filter(|h| *h <= max && *h >= low)
73 .ok_or_else(|| error("invalid browse bound"))?;
74 Ok([low, high])
75 };
76 let view = string("view")?;
77 let mode = string("mode")?;
78 if ![
79 "artists",
80 "tracks",
81 "albums",
82 "compilations",
83 "countries",
84 "genres",
85 "composers",
86 "rated",
87 "favourites",
88 "tags",
89 "label",
90 ]
91 .contains(&view.as_str())
92 || !["none", "years", "alphabet", "both"].contains(&mode.as_str())
93 {
94 return Err(error("invalid browse view or mode"));
95 }
96 let reverse = match v.get("reverse") {
97 None => false,
98 Some(value) if view == "tracks" => value
99 .as_bool()
100 .ok_or_else(|| error("invalid track order"))?,
101 Some(_) => return Err(error("reverse requires the tracks view")),
102 };
103 let label = if view == "label" {
104 Some(
105 EntityRef::parse_token(&string("label")?)
106 .filter(|r| r.kind == EntityKind::Label && r.key.len() <= 4096)
107 .ok_or_else(|| error("invalid browse label reference"))?,
108 )
109 } else {
110 if o.contains_key("label") {
111 return Err(error("label requires the label view"));
112 }
113 None
114 };
115 let composers = if view == "composers" {
116 v["composers"]
117 .as_array()
118 .filter(|rows| rows.len() <= 100)
119 .ok_or_else(|| error("invalid browse composers"))?
120 .iter()
121 .map(|value| {
122 value
123 .as_str()
124 .and_then(EntityRef::parse_token)
125 .filter(|reference| {
126 reference.kind == EntityKind::Artist && reference.key.len() <= 4096
127 })
128 .ok_or_else(|| error("invalid browse composer reference"))
129 })
130 .collect::<Result<Vec<_>, _>>()?
131 } else {
132 if o.contains_key("composers") {
133 return Err(error("composers require the composers view"));
134 }
135 Vec::new()
136 };
137 let ratings = if view == "rated" {
138 let bounds = interval("ratings", 5)?;
139 if bounds[0] == 0 {
140 return Err(error("minimum rating must be at least one"));
141 }
142 Some(bounds)
143 } else {
144 if o.contains_key("ratings") {
145 return Err(error("ratings require the rated view"));
146 }
147 None
148 };
149 let countries = match v.get("countries") {
150 None => Vec::new(),
151 Some(value) => value
152 .as_array()
153 .filter(|a| a.len() <= 256)
154 .ok_or_else(|| error("invalid browse countries"))?
155 .iter()
156 .map(|v| {
157 if v.is_null() {
158 Ok(None)
159 } else {
160 v.as_str()
161 .filter(|s| s.len() <= 256)
162 .map(|s| Some(s.to_owned()))
163 .ok_or_else(|| error("invalid browse country"))
164 }
165 })
166 .collect::<Result<_, _>>()?,
167 };
168 let mut genres = match v.get("genres") {
169 None => Vec::new(),
170 Some(value) => value
171 .as_array()
172 .filter(|a| a.len() <= 256)
173 .ok_or_else(|| error("invalid browse genres"))?
174 .iter()
175 .map(|value| {
176 value
177 .as_str()
178 .filter(|s| !s.trim().is_empty() && s.len() <= 256)
179 .map(str::to_owned)
180 .ok_or_else(|| error("invalid browse genre"))
181 })
182 .collect::<Result<Vec<_>, _>>()?,
183 };
184 let genre = match v.get("genre") {
185 None => None,
186 Some(value) => Some(
187 value
188 .as_str()
189 .filter(|s| s.len() <= 256)
190 .ok_or_else(|| error("invalid browse genre"))?
191 .to_owned(),
192 ),
193 };
194 if let Some(genre) = genre {
195 genres.push(genre);
196 }
197 genres.sort();
198 genres.dedup();
199 let tags = if view == "tags" {
200 if mode != "none" || !countries.is_empty() || !genres.is_empty() {
201 return Err(error(
202 "tag collections require hidden rails and no country/genre criteria",
203 ));
204 }
205 v["tags"]
206 .as_array()
207 .filter(|a| a.len() <= 100)
208 .ok_or_else(|| error("invalid browse tags"))?
209 .iter()
210 .map(|value| {
211 value
212 .as_str()
213 .filter(|s| !s.trim().is_empty() && s.len() <= 256 && !s.contains('\0'))
214 .map(folded)
215 .ok_or_else(|| error("invalid browse tag"))
216 })
217 .collect::<Result<Vec<_>, _>>()?
218 } else {
219 if o.contains_key("tags") {
220 return Err(error("tags require the tags view"));
221 }
222 Vec::new()
223 };
224 let query = string("query")?;
225 if query.len() > 2048 {
226 return Err(error("browse search exceeds 2048 bytes"));
227 }
228 if view == "tracks" && (!countries.is_empty() || !genres.is_empty()) {
229 return Err(error(
230 "track filters do not support country or genre facets",
231 ));
232 }
233 Ok(Filter {
234 view,
235 reverse,
236 mode,
237 years: interval("years", 9999)?,
238 alphabet: interval("alphabet", 26)?,
239 unknown: v["includeUnknownYear"]
240 .as_bool()
241 .ok_or_else(|| error("invalid unknown-year choice"))?,
242 query,
243 countries,
244 genres,
245 ratings,
246 tags,
247 label,
248 composers,
249 })
250}
251#[derive(Default)]
252pub(super) struct Claims {
253 origins: BTreeMap<Id, Option<String>>,
254 prose: BTreeMap<Id, String>,
255}
256impl Claims {
257 pub(super) fn new(catalog: &Catalog, sources: &Sources) -> Self {
258 let mut claims = Self::default();
259 for artist in &catalog.artists {
260 let Some(reference) = EntityRef::of(catalog, EntityKind::Artist, artist.id) else {
261 continue;
262 };
263 for record in sources
264 .about(&reference)
265 .filter(|r| sources.is_trusted(catalog, r))
266 {
267 if let Facts::Artist(facts) = &record.facts {
268 if record.source == crate::sources::MUSICBRAINZ {
269 claims
270 .origins
271 .insert(artist.id, facts.country_code.clone().or(facts.area.clone()));
272 }
273 if let Some(prose) = &facts.summary {
274 claims
275 .prose
276 .entry(artist.id)
277 .or_insert_with(|| prose.text.clone());
278 }
279 }
280 }
281 }
282 claims
283 }
284}
285fn folded(s: &str) -> String {
286 s.trim()
287 .to_lowercase()
288 .chars()
289 .flat_map(crate::text::fold_char)
290 .filter(|c| !('\u{0300}'..='\u{036f}').contains(c))
291 .collect()
292}
293fn initial(s: &str) -> u64 {
294 let first = s.trim().chars().next().unwrap_or('#');
296 let folded = folded(&first.to_string());
297 match folded.bytes().next() {
298 Some(c @ b'a'..=b'z') => u64::from(c - b'a' + 1),
299 _ => 0,
300 }
301}
302fn alphabet(name: &str, f: &Filter) -> bool {
303 !["alphabet", "both"].contains(&f.mode.as_str())
304 || (f.alphabet[0]..=f.alphabet[1]).contains(&initial(name))
305}
306fn annotation(context: &Context, kind: EntityKind, id: Id) -> String {
307 EntityRef::of(context.catalog, kind, id)
308 .and_then(|r| context.data.find(context.owner, &r))
309 .map(|a| {
310 format!(
311 "{} {} {} {}",
312 a.tags.iter().cloned().collect::<Vec<_>>().join(" "),
313 a.note.as_deref().unwrap_or(""),
314 a.rating.map(|r| r.to_string()).unwrap_or_default(),
315 if a.loved { "favori" } else { "" }
316 )
317 })
318 .unwrap_or_default()
319}
320pub(super) fn tracks(f: &Filter, context: &Context) -> Vec<Id> {
322 let c = context.catalog;
323 if f.view == "tracks" {
324 return track_selection(f, context);
325 }
326 if f.view == "tags" {
327 return tagged_tracks(f, context);
328 }
329 let label = f.label.as_ref().and_then(|r| r.resolve(c));
330 if f.label.is_some() && label.is_none() {
331 return Vec::new();
332 }
333 let composer_index = (f.view == "composers").then(|| c.composers_by_release());
334 let selected_composers: BTreeSet<_> = f
335 .composers
336 .iter()
337 .filter_map(|reference| reference.resolve(c))
338 .collect();
339 let artist_view = ["artists", "countries", "genres"].contains(&f.view.as_str());
340 let principal: BTreeSet<_> = c
341 .releases
342 .iter()
343 .filter_map(|r| r.album_artist_id)
344 .collect();
345 let mut artist_text = BTreeMap::new();
346 let mut eligible = BTreeSet::new();
347 for artist in &c.artists {
348 let releases: BTreeSet<_> = c
349 .releases_of_artist(artist.id)
350 .into_iter()
351 .chain(
352 c.releases
353 .iter()
354 .filter(|r| r.album_artist_id == Some(artist.id))
355 .map(|r| r.id),
356 )
357 .collect();
358 let genres: BTreeSet<_> = releases
359 .iter()
360 .flat_map(|id| c.genres_of(EntityKind::Release, *id))
361 .map(|g| g.name.as_str())
362 .collect();
363 let country = context
364 .browse_claims
365 .origins
366 .get(&artist.id)
367 .cloned()
368 .flatten();
369 let valid = (!artist_view || (principal.contains(&artist.id) && alphabet(&artist.name, f)))
370 && (f.countries.is_empty() || f.countries.contains(&country))
371 && (f.genres.is_empty() || f.genres.iter().any(|g| genres.contains(g.as_str())));
372 if valid {
373 eligible.insert(artist.id);
374 }
375 artist_text.insert(
376 artist.id,
377 folded(&format!(
378 "{} {} {} {} {} {} {} {}",
379 artist.name,
380 artist.sort_name,
381 artist.mbid.as_deref().unwrap_or(""),
382 artist.aliases.join(" "),
383 genres.into_iter().collect::<Vec<_>>().join(" "),
384 country.unwrap_or_default(),
385 context
386 .browse_claims
387 .prose
388 .get(&artist.id)
389 .cloned()
390 .unwrap_or_default(),
391 annotation(context, EntityKind::Artist, artist.id)
392 )),
393 );
394 }
395 let term = folded(&f.query);
396 let mut tracks = Vec::new();
397 for release in &c.releases {
398 if let Some(index) = &composer_index {
399 let Some(credited) = index.get(&release.id) else {
400 continue;
401 };
402 if !f.composers.is_empty() && credited.is_disjoint(&selected_composers) {
403 continue;
404 }
405 }
406 if label.is_some_and(|id| !release.label_ids.contains(&id)) {
407 continue;
408 }
409 if f.view == "rated" || f.view == "favourites" {
410 let own = EntityRef::of(c, EntityKind::Release, release.id)
411 .and_then(|r| context.data.find(context.owner, &r));
412 let principal = release
413 .album_artist_id
414 .and_then(|id| EntityRef::of(c, EntityKind::Artist, id))
415 .and_then(|r| context.data.find(context.owner, &r));
416 let rating = own
417 .and_then(|a| a.rating)
418 .unwrap_or(0)
419 .max(principal.and_then(|a| a.rating).unwrap_or(0));
420 if f.ratings
421 .is_some_and(|r| !(r[0]..=r[1]).contains(&u64::from(rating)))
422 || (f.view == "favourites"
423 && !own.is_some_and(|a| a.loved)
424 && !principal.is_some_and(|a| a.loved))
425 {
426 continue;
427 }
428 }
429 if f.view == "compilations" && !release.is_compilation {
430 continue;
431 }
432 if ["years", "both"].contains(&f.mode.as_str())
433 && !release.year.map_or(f.unknown, |y| {
434 (f.years[0]..=f.years[1]).contains(&u64::from(y))
435 })
436 {
437 continue;
438 }
439 if !artist_view && !alphabet(&release.title, f) {
440 continue;
441 }
442 let mut related: BTreeSet<_> = release.album_artist_id.into_iter().collect();
443 related.extend(
444 c.credits_on(EntityKind::Release, release.id)
445 .into_iter()
446 .map(|(a, _)| a.id),
447 );
448 let mut text = format!(
449 "{} {} {} {}",
450 release.title,
451 release.year.map(|y| y.to_string()).unwrap_or_default(),
452 c.genres_of(EntityKind::Release, release.id)
453 .into_iter()
454 .map(|g| g.name.as_str())
455 .collect::<Vec<_>>()
456 .join(" "),
457 annotation(context, EntityKind::Release, release.id)
458 );
459 for id in &release.track_ids {
460 related.extend(
461 c.credits_on(EntityKind::Track, *id)
462 .into_iter()
463 .map(|(a, _)| a.id),
464 );
465 if let Some(file) = c.track(*id).and_then(|t| c.file(t.file_id)) {
466 for (key, values) in &file.tags {
467 text.push_str(&format!(" {key} {}", values.join(" ")));
468 }
469 }
470 }
471 let text = folded(&text);
472 let matches = related.iter().any(|a| {
473 eligible.contains(a)
474 && (term.is_empty()
475 || text.contains(&term)
476 || artist_text.get(a).is_some_and(|s| s.contains(&term)))
477 }) || (!artist_view
478 && related.is_empty()
479 && f.countries.is_empty()
480 && f.genres.is_empty()
481 && (term.is_empty() || text.contains(&term)));
482 if matches {
483 tracks.extend(&release.track_ids);
484 }
485 }
486 tracks
487}
488
489fn tagged_tracks(f: &Filter, context: &Context) -> Vec<Id> {
490 let term = folded(&f.query);
491 let matches = |kind, id| {
492 EntityRef::of(context.catalog, kind, id)
493 .and_then(|r| context.data.find(context.owner, &r))
494 .is_some_and(|a| {
495 a.tags.iter().any(|tag| {
496 let key = folded(tag);
497 (f.tags.is_empty() || f.tags.contains(&key)) && key.contains(&term)
498 })
499 })
500 };
501 context
502 .catalog
503 .tracks
504 .iter()
505 .filter(|track| {
506 matches(EntityKind::Track, track.id)
507 || track
508 .release_id
509 .and_then(|id| context.catalog.release(id))
510 .is_some_and(|release| {
511 matches(EntityKind::Release, release.id)
512 || release
513 .album_artist_id
514 .is_some_and(|id| matches(EntityKind::Artist, id))
515 })
516 })
517 .map(|track| track.id)
518 .collect()
519}
520#[cfg(test)]
521#[path = "browse_tests.rs"]
522mod tests;
523
524fn track_selection(f: &Filter, context: &Context) -> Vec<Id> {
526 let c = context.catalog;
527 let term = folded(&f.query);
528 let has = |value: &str| folded(value).contains(&term);
529 let mut credited = BTreeSet::new();
530 if !term.is_empty() {
531 for credit in &c.credits {
532 if credit.entity_kind == EntityKind::Track
533 && (has(&credit.role)
534 || c.artist(credit.artist_id)
535 .is_some_and(|artist| has(&artist.name)))
536 {
537 credited.insert(credit.entity_id);
538 }
539 }
540 }
541 let mut rows: Vec<_> = c
542 .tracks
543 .iter()
544 .filter(|track| {
545 let release = track.release_id.and_then(|id| c.release(id));
546 if !alphabet(&track.title, f)
547 || (["years", "both"].contains(&f.mode.as_str())
548 && !release.and_then(|r| r.year).map_or(f.unknown, |y| {
549 (f.years[0]..=f.years[1]).contains(&u64::from(y))
550 }))
551 {
552 return false;
553 }
554 term.is_empty()
555 || has(&track.title)
556 || credited.contains(&track.id)
557 || release.is_some_and(|release| {
558 has(&release.title)
559 || release
560 .album_artist_id
561 .and_then(|id| c.artist(id))
562 .is_some_and(|artist| has(&artist.name))
563 })
564 || c.file(track.file_id).is_some_and(|file| {
565 file.tags
566 .iter()
567 .any(|(key, values)| has(key) || values.iter().any(|value| has(value)))
568 })
569 })
570 .map(|track| track.id)
571 .collect();
572 if f.reverse {
573 rows.reverse();
574 }
575 rows
576}