Skip to main content

koan_core/
browse.rs

1//! The library browsers: the album, artist and track listings, narrowed by the
2//! browse filters and the name filter and put in the order chosen. Every front
3//! end offers the same filters and sorts, so the queries they make are built
4//! here, once.
5//!
6//! A shelf's "See all" opens a browser set up as the shelf ([`of_shelf`]), so
7//! the listing it opens on is the one the shelf's preview is the head of.
8
9use crate::db::queries::{
10    AlbumFilter, AlbumOrder, AlbumQuery, ArtistOrder, ArtistQuery, LOCAL_USER, PlayedSince,
11    TrackFilter, TrackOrder,
12};
13use crate::shelves::{self, Shelf, Tracks};
14
15/// What the browsers are narrowed to beyond the name filter. An artist passes
16/// when any of their albums does.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct BrowseFilter {
19    pub favourites: bool,
20    /// Only what was played in the last `shelves::RECENT_DAYS`: the Recently
21    /// Played shelf as a filter.
22    pub recent: bool,
23    /// Only what can play on this device: the Downloaded shelf as a filter.
24    pub downloaded: bool,
25    /// Only albums in a lossless codec. Tracks are narrowed by codec instead.
26    pub lossless: bool,
27    pub codec: Option<String>,
28    pub genre: Option<String>,
29    /// Release year bounds, inclusive. Albums without a date are left out when
30    /// either is set.
31    pub year_from: Option<i32>,
32    pub year_to: Option<i32>,
33}
34
35impl BrowseFilter {
36    /// How many filters are on, as the control showing them counts them.
37    pub fn active_count(&self) -> usize {
38        [
39            self.favourites,
40            self.recent,
41            self.downloaded,
42            self.lossless,
43            self.codec.is_some(),
44            self.genre.is_some(),
45            self.year_from.is_some() || self.year_to.is_some(),
46        ]
47        .into_iter()
48        .filter(|on| *on)
49        .count()
50    }
51
52    fn played(&self) -> Option<PlayedSince> {
53        self.recent
54            .then(|| Shelf::Recent.albums(LOCAL_USER, shelves::now()).played)
55            .flatten()
56    }
57
58    fn albums(&self) -> AlbumFilter<'_> {
59        AlbumFilter {
60            lossless: self.lossless,
61            codec: trimmed(self.codec.as_deref()),
62            year_from: self.year_from,
63            year_to: self.year_to,
64            genre: trimmed(self.genre.as_deref()),
65            on_device: self.downloaded || crate::remote::offline::active(),
66        }
67    }
68}
69
70/// The album browser's orders.
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub enum AlbumSort {
73    /// Newest first: the album you just added is the one you are looking for.
74    #[default]
75    RecentlyAdded,
76    Title,
77    Artist,
78    Year,
79    /// Shuffled by a seed held beside the sort, so narrowing a shuffle does
80    /// not deal it again.
81    Random,
82    /// Only offered with the Recently Played filter, which knows when.
83    LastPlayed,
84    /// Fully on this device first: only offered with the Downloaded filter.
85    Downloaded,
86    /// The closest match to the name filter first: only offered with one.
87    BestMatch,
88}
89
90impl AlbumSort {
91    /// The sorts that mean something for `filter`, and with a name filter
92    /// when `searching`.
93    pub fn offered(filter: &BrowseFilter, searching: bool) -> Vec<AlbumSort> {
94        let mut sorts = Vec::new();
95        if searching {
96            sorts.push(AlbumSort::BestMatch);
97        }
98        if filter.recent {
99            sorts.push(AlbumSort::LastPlayed);
100        }
101        if filter.downloaded {
102            sorts.push(AlbumSort::Downloaded);
103        }
104        sorts.extend([
105            AlbumSort::RecentlyAdded,
106            AlbumSort::Title,
107            AlbumSort::Artist,
108            AlbumSort::Year,
109            AlbumSort::Random,
110        ]);
111        sorts
112    }
113
114    fn order(self, seed: i64) -> AlbumOrder {
115        match self {
116            AlbumSort::RecentlyAdded => AlbumOrder::RecentlyAdded,
117            AlbumSort::Title => AlbumOrder::Title,
118            AlbumSort::Artist => AlbumOrder::ArtistThenDate,
119            AlbumSort::Year => AlbumOrder::YearDesc,
120            AlbumSort::Random => AlbumOrder::Random(seed),
121            AlbumSort::LastPlayed => AlbumOrder::LastPlayed,
122            AlbumSort::Downloaded => AlbumOrder::Downloaded,
123            AlbumSort::BestMatch => AlbumOrder::Relevance,
124        }
125    }
126}
127
128/// The track browser's orders.
129#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
130pub enum TrackSort {
131    /// Artist, then album, disc and track: the library as it would be shelved.
132    #[default]
133    Artist,
134    Title,
135    /// By album title, then in running order.
136    Album,
137    Duration,
138    /// Most recently played first: only offered with the Recently Played filter.
139    LastPlayed,
140    /// The closest match to the name filter first: only offered with one.
141    BestMatch,
142}
143
144impl TrackSort {
145    /// As [`AlbumSort::offered`].
146    pub fn offered(filter: &BrowseFilter, searching: bool) -> Vec<TrackSort> {
147        let mut sorts = Vec::new();
148        if searching {
149            sorts.push(TrackSort::BestMatch);
150        }
151        if filter.recent {
152            sorts.push(TrackSort::LastPlayed);
153        }
154        sorts.extend([
155            TrackSort::Artist,
156            TrackSort::Title,
157            TrackSort::Album,
158            TrackSort::Duration,
159        ]);
160        sorts
161    }
162
163    /// The order, and whether it runs descending as `filter_tracks` takes it.
164    fn order(self) -> (TrackOrder, bool) {
165        match self {
166            TrackSort::Artist => (TrackOrder::ArtistAlbumDiscTrack, false),
167            TrackSort::Title => (TrackOrder::Title, false),
168            TrackSort::Album => (TrackOrder::Album, false),
169            TrackSort::Duration => (TrackOrder::Duration, false),
170            TrackSort::LastPlayed => (TrackOrder::LastPlayed, true),
171            TrackSort::BestMatch => (TrackOrder::Relevance, false),
172        }
173    }
174}
175
176/// A name filter someone typed, or nothing: whitespace is not a filter.
177pub fn trimmed(search: Option<&str>) -> Option<&str> {
178    search.map(str::trim).filter(|s| !s.is_empty())
179}
180
181/// The album browser's listing, or one artist's albums with `artist`.
182/// `seed` fixes the shuffle under [`AlbumSort::Random`].
183pub fn albums<'a>(
184    filter: &'a BrowseFilter,
185    search: Option<&'a str>,
186    sort: AlbumSort,
187    seed: i64,
188) -> AlbumQuery<'a> {
189    AlbumQuery {
190        search: trimmed(search),
191        order: sort.order(seed),
192        favourites_of: filter.favourites.then_some(LOCAL_USER),
193        played: filter.played(),
194        filter: filter.albums(),
195        ..Default::default()
196    }
197}
198
199/// The artist browser's listing. It has no sort of its own: Recently Played's
200/// order with that filter, else the closest match to the name filter, and by
201/// name without one.
202pub fn artists<'a>(filter: &'a BrowseFilter, search: Option<&'a str>) -> ArtistQuery<'a> {
203    let played = filter.played();
204    ArtistQuery {
205        search: trimmed(search),
206        favourites_of: filter.favourites.then_some(LOCAL_USER),
207        order: if played.is_some() {
208            ArtistOrder::LastPlayed
209        } else {
210            ArtistOrder::Relevance
211        },
212        played,
213        filter: filter.albums(),
214        ..Default::default()
215    }
216}
217
218/// The track browser's listing. `filter.lossless` does not apply to tracks.
219pub fn tracks(filter: &BrowseFilter, search: Option<&str>, sort: TrackSort) -> Tracks {
220    let (order, descending) = sort.order();
221    let (user, now) = (LOCAL_USER, shelves::now());
222    Tracks {
223        filter: TrackFilter {
224            search: trimmed(search).and_then(|q| Shelf::Search(q).tracks(user, now).filter.search),
225            favourites_of: filter.favourites.then_some(user),
226            played: filter.played(),
227            codec: trimmed(filter.codec.as_deref()).map(str::to_owned),
228            genre: trimmed(filter.genre.as_deref()).map(str::to_owned),
229            year_start: filter.year_from,
230            year_end: filter.year_to,
231            on_device: filter.downloaded || crate::remote::offline::active(),
232            ..Default::default()
233        },
234        order,
235        descending,
236    }
237}
238
239/// A browser set up as `shelf`, for its "See all": the filter, and the album
240/// and track sorts that read as the shelf does. A search's query becomes the
241/// name filter, which the caller carries.
242pub fn of_shelf(shelf: Shelf) -> (BrowseFilter, AlbumSort, TrackSort) {
243    match shelf {
244        Shelf::Favourites => (
245            BrowseFilter {
246                favourites: true,
247                ..Default::default()
248            },
249            AlbumSort::Artist,
250            TrackSort::Artist,
251        ),
252        Shelf::Recent => (
253            BrowseFilter {
254                recent: true,
255                ..Default::default()
256            },
257            AlbumSort::LastPlayed,
258            TrackSort::LastPlayed,
259        ),
260        Shelf::Search(_) => (
261            BrowseFilter::default(),
262            AlbumSort::BestMatch,
263            TrackSort::BestMatch,
264        ),
265        Shelf::Downloaded => (
266            BrowseFilter {
267                downloaded: true,
268                ..Default::default()
269            },
270            AlbumSort::Downloaded,
271            TrackSort::Artist,
272        ),
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn sorts_are_offered_where_they_mean_something() {
282        let none = BrowseFilter::default();
283        assert_eq!(
284            AlbumSort::offered(&none, false)[0],
285            AlbumSort::RecentlyAdded
286        );
287        assert!(!AlbumSort::offered(&none, false).contains(&AlbumSort::LastPlayed));
288        let recent = BrowseFilter {
289            recent: true,
290            downloaded: true,
291            ..Default::default()
292        };
293        assert_eq!(
294            AlbumSort::offered(&recent, true)[..3],
295            [
296                AlbumSort::BestMatch,
297                AlbumSort::LastPlayed,
298                AlbumSort::Downloaded
299            ]
300        );
301        assert_eq!(TrackSort::offered(&recent, false)[0], TrackSort::LastPlayed);
302    }
303
304    #[test]
305    fn a_year_range_counts_once() {
306        let f = BrowseFilter {
307            year_from: Some(1990),
308            year_to: Some(1999),
309            lossless: true,
310            ..Default::default()
311        };
312        assert_eq!(f.active_count(), 2);
313        assert_eq!(BrowseFilter::default().active_count(), 0);
314    }
315
316    #[test]
317    fn a_blank_name_filter_is_no_filter() {
318        let f = BrowseFilter::default();
319        assert_eq!(albums(&f, Some("  "), AlbumSort::Title, 0).search, None);
320        assert_eq!(artists(&f, Some(" x ")).search, Some("x"));
321    }
322}