Skip to main content

chilen_backend/music_lib/state/
playlist.rs

1use std::{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: Box<str>,
22    pub tracks: Box<[Arc<Track>]>,
23    pub duration: Duration,
24    pub cover: Cover,
25    /// Unique per-session 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: Box<[u64]>,
31}
32
33impl Playlist {
34    pub(super) fn load(
35        lib: &mut MusicLibrary,
36        loaded: PlaylistStored,
37        config: indexer::Config,
38    ) -> Self {
39        let result = lib.find_tracks_by_hashes(&loaded.track_hashes);
40        if !result.unmatched.is_empty() {
41            warn!(
42                "{} missing tracks in playlist {}",
43                result.unmatched.len(),
44                loaded.name
45            );
46        }
47        let duration = result.matched.iter().map(|t| t.duration).sum();
48
49        #[cfg(not(test))]
50        let cover =
51            get_playlist_cover(&loaded.name, config, &result.matched).unwrap_or(Cover::none());
52        #[cfg(test)]
53        let cover = if config.cache_mode != CacheMode::Disabled {
54            get_playlist_cover(&loaded.name, config, &result.matched).unwrap_or(Cover::none())
55        } else {
56            Cover::none()
57        };
58
59        Self {
60            name: loaded.name.into(),
61            tracks: result.matched.into(),
62            duration,
63            unmatched: result.unmatched.into(),
64            id: lib.get_playlist_id(),
65            cover,
66        }
67    }
68
69    pub(crate) fn remove_tracks(&mut self, mut ids: Vec<usize>) -> Result<(), Error> {
70        ids.sort_unstable();
71
72        if ids.windows(2).any(|pair| pair[0] == pair[1]) {
73            return Err(Error::DuplicateItems);
74        }
75        if ids.iter().any(|&i| i >= self.tracks.len()) {
76            return Err(Error::IndexOutOfBounds);
77        }
78
79        let mut ids = ids.into_iter().peekable();
80        let mut tracks = std::mem::take(&mut self.tracks).into_vec();
81        let mut index = 0;
82
83        tracks.retain(|_| {
84            let remove = ids.peek() == Some(&index);
85            if remove {
86                ids.next();
87            }
88            index += 1;
89            !remove
90        });
91
92        self.tracks = tracks.into_boxed_slice();
93
94        Ok(())
95    }
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize)]
99pub(crate) struct PlaylistStored {
100    name: String,
101    track_hashes: Vec<u64>,
102}
103
104impl From<Playlist> for PlaylistStored {
105    fn from(value: Playlist) -> Self {
106        let mut track_hashes: Vec<_> = value.tracks.iter().map(|t| t.hash_self()).collect();
107        track_hashes.extend(value.unmatched);
108        Self {
109            name: value.name.to_string(),
110            track_hashes,
111        }
112    }
113}