Skip to main content

chilen_backend/music_lib/state/
playlist.rs

1use std::{collections::HashSet, hash::Hash, sync::Arc, time::Duration};
2
3use log::warn;
4use serde::{Deserialize, Serialize};
5
6#[cfg(test)]
7use crate::music_lib::indexer::CacheMode;
8use crate::{
9    Error,
10    music_lib::{
11        indexer::{
12            self,
13            covers::{Cover, get_playlist_cover},
14        },
15        state::{MusicLibrary, Track},
16    },
17};
18
19#[derive(Clone, Debug, PartialEq, Eq, Hash)]
20pub struct Playlist {
21    pub name: String,
22    pub tracks: Vec<Arc<Track>>,
23    pub duration: Duration,
24    pub cover: Cover,
25    /// Unique ID for identifying the playlist in the library.
26    ///
27    /// This ID is not dependent on the contents of the playlist or it's name, and once a playlist
28    /// with a specific ID has been created, no new playlist can reclaim it.
29    pub id: u64,
30    pub(crate) unmatched: Vec<u64>,
31}
32
33impl Playlist {
34    pub(super) fn load(lib: &MusicLibrary, loaded: ConfPlaylist, config: indexer::Config) -> Self {
35        let result = lib.tracks_from_hashes(loaded.track_hashes);
36        if !result.unmatched.is_empty() {
37            warn!(
38                "{} missing tracks in playlist {}",
39                result.unmatched.len(),
40                loaded.name
41            );
42        }
43        let duration = result.matched.iter().map(|t| t.duration).sum();
44
45        #[cfg(not(test))]
46        let cover =
47            get_playlist_cover(&loaded.name, config, &result.matched).unwrap_or(Cover::none());
48        #[cfg(test)]
49        let cover = if config.cache_mode != CacheMode::Disabled {
50            get_playlist_cover(&loaded.name, config, &result.matched).unwrap_or(Cover::none())
51        } else {
52            Cover::none()
53        };
54
55        Self {
56            name: loaded.name,
57            tracks: result.matched,
58            duration,
59            unmatched: result.unmatched,
60            id: loaded.id,
61            cover,
62        }
63    }
64
65    pub(crate) fn remove_tracks(&mut self, mut ids: Vec<usize>) -> Result<(), Error> {
66        ids.sort();
67        let mut unique = ids.clone();
68        unique.dedup();
69        if unique != ids {
70            return Err(Error::DuplicateItems);
71        }
72        if ids.iter().find(|i| i >= &&self.tracks.len()).is_some() {
73            return Err(Error::IndexOutOfBounds);
74        }
75        let remove_set: HashSet<usize> = ids.into_iter().collect();
76        self.tracks = self
77            .tracks
78            .drain(..)
79            .enumerate()
80            .filter_map(|(i, t)| {
81                if remove_set.contains(&i) {
82                    None
83                } else {
84                    Some(t)
85                }
86            })
87            .collect();
88        Ok(())
89    }
90}
91
92#[derive(Clone, Debug, Serialize, Deserialize)]
93pub(crate) struct ConfPlaylist {
94    name: String,
95    id: u64,
96    track_hashes: Vec<u64>,
97}
98
99impl From<Playlist> for ConfPlaylist {
100    fn from(value: Playlist) -> Self {
101        let mut track_hashes: Vec<_> = value.tracks.iter().map(|t| t.hash_self()).collect();
102        track_hashes.extend(value.unmatched);
103        Self {
104            name: value.name,
105            id: value.id,
106            track_hashes,
107        }
108    }
109}