Skip to main content

chilen_backend/music_lib/state/library/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use std::{
5    collections::{HashMap, HashSet},
6    hash::Hash,
7    path::{Path, PathBuf},
8    sync::Arc,
9    time::Duration,
10};
11
12use icu::collator::CollatorBorrowed;
13pub use lofty::tag::items::Timestamp;
14use log::{error, trace};
15use serde::{Deserialize, Serialize};
16
17#[cfg(test)]
18use crate::music_lib::indexer::CacheMode;
19use crate::{
20    COLLATOR, Error, Event, get_config,
21    music_lib::{
22        indexer::{
23            self,
24            covers::{Cover, get_playlist_cover},
25        },
26        state::{ConfPlaylist, Playlist, Progress, Track},
27        tracks_from_m3u8,
28    },
29};
30
31const DEFAULT_PLAYLIST_NAME: &str = "New Playlist";
32
33#[derive(Clone, Debug, PartialEq)]
34pub struct HashMatchingResult {
35    pub matched: Vec<Arc<Track>>,
36    pub unmatched: Vec<u64>,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Hash)]
40pub struct Album {
41    pub title: String,
42    pub cover: Cover,
43    pub artists: Vec<String>,
44    pub tracks: Vec<Arc<Track>>,
45    pub date: Option<Timestamp>,
46    pub duration: Duration,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Hash)]
50pub struct Artist {
51    pub name: String,
52    pub cover: Cover,
53    pub tracks: Vec<Arc<Track>>,
54    pub albums: Vec<Arc<Album>>,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Hash)]
58pub struct Genre {
59    pub name: String,
60    pub cover: Cover,
61    pub artists: Vec<Arc<Artist>>,
62    pub albums: Vec<Arc<Album>>,
63    pub tracks: Vec<Arc<Track>>,
64}
65
66#[derive(Clone, Debug, PartialEq)]
67pub struct MusicLibrary {
68    pub playlists: Vec<Arc<Playlist>>,
69    pub tracks: Vec<Arc<Track>>,
70    pub albums: Vec<Arc<Album>>,
71    pub artists: Vec<Arc<Artist>>,
72    pub genres: Vec<Arc<Genre>>,
73    playlist_id_counter: u64,
74    tracks_by_path: HashMap<String, Arc<Track>>,
75    tracks_by_hash: HashMap<u64, Arc<Track>>,
76    playlists_by_name: HashMap<String, Arc<Playlist>>,
77    playlists_by_id: HashMap<u64, Arc<Playlist>>,
78    artists_by_name: HashMap<String, Arc<Artist>>,
79    albums_by_title: HashMap<String, Arc<Album>>,
80    genres_by_name: HashMap<String, Arc<Genre>>,
81}
82
83// TODO: Fallback alphabetic sorting w/out the collator
84impl MusicLibrary {
85    fn sort_tracks_alphabetically(
86        tracks: &mut [Arc<Track>],
87        collator: Option<&Arc<CollatorBorrowed<'_>>>,
88    ) {
89        if let Some(collator) = collator {
90            tracks.sort_by(|t1, t2| {
91                collator.compare(
92                    t1.title.as_deref().unwrap_or(""),
93                    t2.title.as_deref().unwrap_or(""),
94                )
95            });
96        }
97    }
98
99    fn sort_tracks_chronologically(
100        tracks: &mut [Arc<Track>],
101        collator: Option<&Arc<CollatorBorrowed<'_>>>,
102    ) {
103        tracks.sort_by(|a, b| match (a.date, b.date) {
104            (Some(ad), Some(bd)) => bd.cmp(&ad),
105            (Some(_), None) => std::cmp::Ordering::Less,
106            (None, Some(_)) => std::cmp::Ordering::Greater,
107            (None, None) => {
108                if let Some(collator) = collator {
109                    collator.compare(
110                        a.title.as_deref().unwrap_or(""),
111                        b.title.as_deref().unwrap_or(""),
112                    )
113                } else {
114                    std::cmp::Ordering::Equal
115                }
116            }
117        });
118    }
119
120    fn sort_tracks_in_album(
121        tracks: &mut [Arc<Track>],
122        collator: Option<&Arc<CollatorBorrowed<'_>>>,
123    ) {
124        tracks.sort_by(|a, b| match (a.track, b.track) {
125            (Some(at), Some(bt)) => at.cmp(&bt),
126            (Some(_), None) => std::cmp::Ordering::Less,
127            (None, Some(_)) => std::cmp::Ordering::Greater,
128            (None, None) => {
129                if let Some(c) = collator {
130                    c.compare(
131                        a.title.as_deref().unwrap_or(""),
132                        b.title.as_deref().unwrap_or(""),
133                    )
134                } else {
135                    std::cmp::Ordering::Equal
136                }
137            }
138        });
139    }
140
141    fn sort_albums_alphabetically(
142        albums: &mut [Arc<Album>],
143        collator: Option<&Arc<CollatorBorrowed<'_>>>,
144    ) {
145        if let Some(collator) = collator {
146            albums.sort_by(|a1, a2| collator.compare(&a1.title, &a2.title));
147        }
148    }
149
150    fn sort_albums_chronologically(
151        albums: &mut [Arc<Album>],
152        collator: Option<&Arc<CollatorBorrowed<'_>>>,
153    ) {
154        albums.sort_by(|a, b| match (a.date, b.date) {
155            (Some(ad), Some(bd)) => bd.cmp(&ad),
156            (Some(_), None) => std::cmp::Ordering::Less,
157            (None, Some(_)) => std::cmp::Ordering::Greater,
158            (None, None) => {
159                if let Some(c) = collator {
160                    c.compare(&a.title, &b.title)
161                } else {
162                    std::cmp::Ordering::Equal
163                }
164            }
165        });
166    }
167
168    fn sort_artists(artists: &mut [Arc<Artist>], collator: Option<&Arc<CollatorBorrowed<'_>>>) {
169        if let Some(collator) = collator {
170            artists.sort_by(|a1, a2| collator.compare(&a1.name, &a2.name));
171        }
172    }
173
174    fn sort_playlists(&mut self) {
175        let guard = COLLATOR.read().unwrap();
176        if let Some(collator) = guard.as_ref() {
177            self.playlists
178                .sort_by(|p1, p2| collator.compare(&p1.name, &p2.name));
179        }
180    }
181
182    pub(super) fn new(tracks: Vec<Track>) -> Self {
183        crate::send_event(Event::LoadProgressChanged(Progress::RebuildingLibrary));
184
185        let mut tracks: Vec<_> = tracks.into_iter().map(Arc::new).collect();
186
187        let guard = COLLATOR.read().unwrap();
188        let collator = guard.as_ref();
189        Self::sort_tracks_alphabetically(&mut tracks, collator);
190
191        let album_titles: HashSet<_> = tracks.iter().flat_map(|t| &t.album).collect();
192        let artist_names: HashSet<_> = tracks
193            .iter()
194            .flat_map(|t| t.artists.as_ref().into_iter().flatten())
195            .collect();
196        let genre_names: HashSet<_> = tracks
197            .iter()
198            .flat_map(|t| t.genres.as_ref().into_iter().flatten())
199            .collect();
200
201        let mut tracks_by_artist: HashMap<&String, HashSet<Arc<Track>>> =
202            HashMap::with_capacity(artist_names.len());
203        let mut tracks_by_album: HashMap<&String, HashSet<Arc<Track>>> =
204            HashMap::with_capacity(album_titles.len());
205        let mut tracks_by_genre: HashMap<&String, HashSet<Arc<Track>>> =
206            HashMap::with_capacity(genre_names.len());
207
208        for track in &tracks {
209            if let Some(artists) = &track.artists {
210                for artist in artists {
211                    if let Some(val) = tracks_by_artist.get_mut(artist) {
212                        val.insert(track.clone());
213                    } else {
214                        tracks_by_artist.insert(artist, [track.clone()].into_iter().collect());
215                    }
216                }
217            }
218            if let Some(album) = &track.album {
219                if let Some(val) = tracks_by_album.get_mut(album) {
220                    val.insert(track.clone());
221                } else {
222                    tracks_by_album.insert(album, [track.clone()].into_iter().collect());
223                }
224            }
225            if let Some(genres) = &track.genres {
226                for genre in genres {
227                    if let Some(val) = tracks_by_genre.get_mut(genre) {
228                        val.insert(track.clone());
229                    } else {
230                        tracks_by_genre.insert(genre, [track.clone()].into_iter().collect());
231                    }
232                }
233            }
234        }
235
236        let mut albums: Vec<_> = album_titles
237            .into_iter()
238            .map(|title| {
239                let mut tracks: Vec<_> = tracks_by_album[title].clone().into_iter().collect();
240                Self::sort_tracks_in_album(&mut tracks, collator);
241
242                let mut artists: Vec<_> = tracks
243                    .iter()
244                    .flat_map(|t| t.artists.clone().into_iter().flatten())
245                    .collect::<HashSet<String>>()
246                    .into_iter()
247                    .collect();
248                if let Some(collator) = collator {
249                    artists.sort_by(|a1, a2| collator.compare(a1, a2));
250                }
251
252                let mut counts: HashMap<Cover, usize> = HashMap::with_capacity(tracks.len());
253                for track in &tracks {
254                    *counts.entry(track.cover.clone()).or_insert(0) += 1;
255                }
256                let commonest = counts.into_iter().max_by_key(|(_, c)| *c).map(|(k, _)| k);
257                let cover = commonest.unwrap_or(tracks[0].cover.clone());
258
259                let date = tracks.iter().filter_map(|t| t.date).max();
260                let duration = tracks.iter().map(|t| t.duration).sum();
261
262                Arc::new(Album {
263                    title: title.to_string(),
264                    cover,
265                    tracks,
266                    artists,
267                    date,
268                    duration,
269                })
270            })
271            .collect();
272        Self::sort_albums_alphabetically(&mut albums, collator);
273
274        let albums_by_title: HashMap<String, Arc<Album>> = albums
275            .iter()
276            .map(|a| (a.title.clone(), a.clone()))
277            .collect();
278
279        let mut albums_by_artist: HashMap<&String, HashSet<Arc<Album>>> =
280            HashMap::with_capacity(artist_names.len());
281        for album in &albums {
282            for artist in &album.artists {
283                if let Some(val) = albums_by_artist.get_mut(&artist) {
284                    val.insert(album.clone());
285                } else {
286                    albums_by_artist.insert(artist, [album.clone()].into_iter().collect());
287                }
288            }
289        }
290
291        let mut artists: Vec<_> = artist_names
292            .into_iter()
293            .map(|name| {
294                let mut tracks: Vec<_> = tracks_by_artist[name].clone().into_iter().collect();
295                // TODO: Sort tracks chronologically if possible, then fall back to track index in
296                // albums, then to alphabetic sorting
297                Self::sort_tracks_chronologically(&mut tracks, collator);
298
299                let mut albums = if let Some(albums) = albums_by_artist.get(name) {
300                    albums.iter().cloned().collect()
301                } else {
302                    Vec::new()
303                };
304                Self::sort_albums_chronologically(&mut albums, collator);
305
306                let cover = tracks
307                    .first()
308                    .map(|t| t.cover.clone())
309                    .unwrap_or(Cover::none());
310
311                Arc::new(Artist {
312                    name: name.to_string(),
313                    cover,
314                    tracks,
315                    albums,
316                })
317            })
318            .collect();
319        Self::sort_artists(&mut artists, collator);
320
321        let mut artists_by_album: HashMap<&String, HashSet<Arc<Artist>>> =
322            HashMap::with_capacity(albums.len());
323        for artist in &artists {
324            for album in &artist.albums {
325                if let Some(val) = artists_by_album.get_mut(&album.title) {
326                    val.insert(artist.clone());
327                } else {
328                    artists_by_album.insert(&album.title, [artist.clone()].into_iter().collect());
329                }
330            }
331        }
332
333        let mut albums_by_genre: HashMap<&String, HashSet<Arc<Album>>> =
334            HashMap::with_capacity(albums.len());
335        for album in &albums {
336            for track in &album.tracks {
337                if let Some(genres) = &track.genres {
338                    for genre in genres {
339                        if let Some(val) = albums_by_genre.get_mut(genre) {
340                            val.insert(album.clone());
341                        } else {
342                            albums_by_genre.insert(genre, [album.clone()].into_iter().collect());
343                        }
344                    }
345                }
346            }
347        }
348
349        let mut artists_by_genre: HashMap<&String, HashSet<Arc<Artist>>> =
350            HashMap::with_capacity(artists.len());
351        for artist in &artists {
352            for track in &artist.tracks {
353                if let Some(genres) = &track.genres {
354                    for genre in genres {
355                        if let Some(val) = artists_by_genre.get_mut(genre) {
356                            val.insert(artist.clone());
357                        } else {
358                            artists_by_genre.insert(genre, [artist.clone()].into_iter().collect());
359                        }
360                    }
361                }
362            }
363        }
364
365        let mut genres: Vec<_> = genre_names
366            .into_iter()
367            .map(|name| {
368                let mut tracks: Vec<_> = tracks_by_genre[name].clone().into_iter().collect();
369                Self::sort_tracks_alphabetically(&mut tracks, collator);
370
371                let mut counts: HashMap<Cover, usize> = HashMap::with_capacity(tracks.len());
372                for track in &tracks {
373                    *counts.entry(track.cover.clone()).or_insert(0) += 1;
374                }
375                let commonest = counts.into_iter().max_by_key(|(_, c)| *c).map(|(k, _)| k);
376                let cover = commonest.unwrap_or(tracks[0].cover.clone());
377
378                let mut artists: Vec<_> = artists_by_genre[name].clone().into_iter().collect();
379                Self::sort_artists(&mut artists, collator);
380
381                let mut albums: Vec<_> = albums_by_genre[name].clone().into_iter().collect();
382                Self::sort_albums_alphabetically(&mut albums, collator);
383
384                Arc::new(Genre {
385                    name: name.to_string(),
386                    cover,
387                    tracks,
388                    albums,
389                    artists,
390                })
391            })
392            .collect();
393
394        let genres_by_name: HashMap<String, Arc<Genre>> =
395            genres.iter().map(|g| (g.name.clone(), g.clone())).collect();
396
397        if let Some(collator) = collator {
398            genres.sort_by(|g1, g2| collator.compare(&g1.name, &g2.name));
399        }
400
401        let mut track_path_map: HashMap<_, _> = HashMap::with_capacity(tracks.len());
402        for t in tracks.iter() {
403            track_path_map.insert(t.path.to_string_lossy().to_string(), t.clone());
404        }
405
406        let mut track_hash_map: HashMap<_, _> = HashMap::with_capacity(tracks.len());
407        for t in tracks.iter() {
408            track_hash_map.insert(t.hash_self(), t.clone());
409        }
410
411        let artists_by_name: HashMap<String, Arc<Artist>> = artists
412            .iter()
413            .map(|a| (a.name.clone(), a.clone()))
414            .collect();
415
416        Self {
417            playlists: Vec::new(),
418            tracks,
419            albums,
420            artists,
421            genres,
422            playlist_id_counter: 0,
423            tracks_by_path: track_path_map,
424            tracks_by_hash: track_hash_map,
425            playlists_by_name: HashMap::new(),
426            playlists_by_id: HashMap::new(),
427            artists_by_name,
428            albums_by_title,
429            genres_by_name,
430        }
431    }
432
433    pub(super) fn load(
434        loaded: ConfMusicLibrary,
435        tracks: Vec<Track>,
436        config: indexer::Config,
437    ) -> Self {
438        crate::send_event(Event::LoadProgressChanged(Progress::RestoringState));
439
440        let mut lib = Self::new(tracks);
441        let mut playlists = Vec::with_capacity(loaded.playlists.len());
442        lib.playlists_by_name = HashMap::with_capacity(loaded.playlists.len());
443        lib.playlists_by_id = HashMap::with_capacity(loaded.playlists.len());
444        for p in loaded.playlists {
445            let playlist = Arc::new(Playlist::load(&lib, p, config));
446            playlists.push(playlist.clone());
447            lib.playlists_by_name
448                .insert(playlist.name.clone(), playlist.clone());
449            lib.playlists_by_id.insert(playlist.id, playlist);
450        }
451
452        lib.playlists = playlists;
453        lib.sort_playlists();
454        lib.playlist_id_counter = loaded.playlist_id_counter;
455
456        lib
457    }
458
459    #[cfg(test)]
460    pub(crate) fn new_testing(tracks: Vec<Track>) -> Self {
461        Self::new(tracks)
462    }
463
464    fn get_playlist_id(&mut self) -> u64 {
465        self.playlist_id_counter += 1;
466        self.playlist_id_counter
467    }
468
469    fn check_name(&self, name: &str) -> Result<(), Error> {
470        let name = name.trim();
471        if name.is_empty() {
472            return Err(Error::EmptyName);
473        }
474        if self.find_playlist_by_name(name).is_some() {
475            error!("A playlist with name \"{name}\" already exists");
476            return Err(Error::PlaylistExists);
477        }
478        Ok(())
479    }
480
481    pub fn find_track_by_path(&self, path: &Path) -> Option<Arc<Track>> {
482        self.tracks_by_path
483            .get(&path.to_string_lossy().to_string())
484            .cloned()
485    }
486
487    pub fn find_artist(&self, name: &str) -> Option<&Arc<Artist>> {
488        self.artists_by_name.get(name)
489    }
490
491    pub fn find_playlist_by_name(&self, name: &str) -> Option<&Arc<Playlist>> {
492        self.playlists_by_name.get(name)
493    }
494
495    pub fn find_playlist_by_id(&self, id: u64) -> Option<&Arc<Playlist>> {
496        self.playlists_by_id.get(&id)
497    }
498
499    pub fn find_album(&self, title: &str) -> Option<&Arc<Album>> {
500        self.albums_by_title.get(title)
501    }
502
503    pub fn find_genre(&self, name: &str) -> Option<&Arc<Genre>> {
504        self.genres_by_name.get(name)
505    }
506
507    /// Returns the default playlist name ("New Playlist").
508    ///
509    /// If a playlist with the default name exists, then a number will be added to the end of the
510    /// playlist name so it's unique, eg. "New Playlist 1", "New Playlist 2", etc.
511    pub fn get_default_playlist_name(&self) -> String {
512        let mut i = 0;
513        let mut playlist_name = DEFAULT_PLAYLIST_NAME.to_string();
514        while self.find_playlist_by_name(&playlist_name).is_some() {
515            i += 1;
516            playlist_name = format!("{DEFAULT_PLAYLIST_NAME} {i}");
517        }
518        playlist_name
519    }
520
521    /// Retrieves tracks corresponding to a list of provided hashes.
522    pub fn tracks_from_hashes(&self, hashes: Vec<u64>) -> HashMatchingResult {
523        // In most cases all tracks will match
524        let mut tracks = Vec::with_capacity(hashes.len());
525        let mut unmatched = Vec::new();
526        for hash in hashes {
527            if let Some(track) = self.tracks_by_hash.get(&hash) {
528                tracks.push(track.clone());
529            } else {
530                unmatched.push(hash);
531            }
532        }
533        tracks.shrink_to_fit();
534        HashMatchingResult {
535            matched: tracks,
536            unmatched,
537        }
538    }
539
540    pub fn remove_playlists(&mut self, mut playlists: Vec<String>) -> Result<(), Error> {
541        playlists.sort();
542        let mut unique = playlists.clone();
543        unique.dedup();
544        if unique != playlists {
545            return Err(Error::DuplicateItems);
546        }
547
548        for name in &playlists {
549            if let Some(index) = self.playlists.iter().position(|p| p.name == *name) {
550                let id = self.playlists[index].id;
551                self.playlists.remove(index);
552                self.playlists_by_name.remove(name);
553                self.playlists_by_id.remove(&id);
554            } else {
555                return Err(Error::UnknownPlaylist(name.to_string()));
556            }
557        }
558
559        crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
560        Ok(())
561    }
562
563    pub fn create_playlist(
564        &mut self,
565        name: String,
566        track_paths: &Option<Vec<PathBuf>>,
567    ) -> Result<(), Error> {
568        trace!("Creating a new playlist \"{name}\" from a list of tracks");
569
570        let name = name.trim();
571        self.check_name(name)?;
572
573        let tracks = if let Some(tracks) = track_paths {
574            let mut out = Vec::with_capacity(tracks.len());
575            for path in tracks {
576                if let Some(track) = self.find_track_by_path(path) {
577                    out.push(track);
578                } else {
579                    error!("The track {path:?} was not found in the music library");
580                    return Err(Error::UnknownTrackPath(path.to_path_buf()));
581                }
582            }
583            out
584        } else {
585            Vec::new()
586        };
587
588        let duration = tracks.iter().map(|t| t.duration).sum();
589        let config = get_config();
590
591        #[cfg(not(test))]
592        let cover =
593            get_playlist_cover(name, config.library.indexer, &tracks).unwrap_or(Cover::none());
594        #[cfg(test)]
595        let cover = if config.library.indexer.cache_mode != CacheMode::Disabled {
596            get_playlist_cover(name, config.library.indexer, &tracks).unwrap_or(Cover::none())
597        } else {
598            Cover::none()
599        };
600
601        let playlist = Arc::new(Playlist {
602            name: name.to_string(),
603            tracks,
604            duration,
605            unmatched: Vec::new(),
606            id: self.get_playlist_id(),
607            cover,
608        });
609        self.playlists.push(playlist.clone());
610        self.playlists_by_name.insert(name.to_string(), playlist);
611        self.sort_playlists();
612        crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
613        Ok(())
614    }
615
616    pub fn rename_playlist(&mut self, source: &str, target: &str) -> Result<(), Error> {
617        let source = source.trim();
618        let target = target.trim();
619        trace!("Renaming playlist \"{source}\" to \"{target}\"");
620        self.check_name(target)?;
621
622        let src_playlist_index = match self.playlists.iter().position(|p| p.name == source) {
623            Some(pos) => pos,
624            None => return Err(Error::UnknownPlaylist(source.to_string())),
625        };
626        let src_playlist = self.playlists[src_playlist_index].clone();
627        let playlist_id = src_playlist.id;
628        let mut playlist = src_playlist.as_ref().clone();
629        playlist.name = target.to_string();
630
631        self.playlists.remove(src_playlist_index);
632        self.playlists_by_name.remove(source);
633        self.playlists_by_id.remove(&playlist_id);
634        let playlist = Arc::new(playlist);
635        self.playlists.push(playlist.clone());
636        self.playlists_by_name
637            .insert(target.to_string(), playlist.clone());
638        self.playlists_by_id.insert(playlist_id, playlist);
639        self.sort_playlists();
640
641        crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
642        Ok(())
643    }
644
645    pub fn add_tracks(&mut self, name: &str, tracks: Vec<PathBuf>) -> Result<(), Error> {
646        let name = name.trim();
647        trace!("Adding tracks to playlist \"{name}\"");
648
649        let playlist_index = match self.playlists.iter().position(|p| p.name == *name) {
650            Some(i) => i,
651            None => return Err(Error::UnknownPlaylist(name.to_string())),
652        };
653
654        let mut out = Vec::with_capacity(tracks.len());
655        for path in tracks {
656            if let Some(track) = self.find_track_by_path(&path) {
657                out.push(track.clone());
658            } else {
659                return Err(Error::UnknownTrackPath(path.to_path_buf()));
660            }
661        }
662
663        let mut playlist = self.playlists[playlist_index].as_ref().clone();
664        self.playlists.remove(playlist_index);
665        self.playlists_by_name.remove(&playlist.name);
666        self.playlists_by_id.remove(&playlist.id);
667
668        playlist.tracks.append(&mut out);
669
670        let playlist = Arc::new(playlist);
671        self.playlists.push(playlist.clone());
672        self.playlists_by_name
673            .insert(playlist.name.clone(), playlist.clone());
674        self.playlists_by_id.insert(playlist.id, playlist);
675        self.sort_playlists();
676
677        crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
678        Ok(())
679    }
680
681    pub fn remove_tracks(&mut self, name: &str, tracks: Vec<usize>) -> Result<(), Error> {
682        let name = name.trim();
683        trace!("Removing tracks from playlist \"{name}\"");
684
685        let playlist_index = match self.playlists.iter().position(|p| p.name == *name) {
686            Some(i) => i,
687            None => return Err(Error::UnknownPlaylist(name.to_string())),
688        };
689        let mut playlist = self.playlists[playlist_index].as_ref().clone();
690
691        self.playlists.remove(playlist_index);
692        self.playlists_by_name.remove(&playlist.name);
693        self.playlists_by_id.remove(&playlist.id);
694
695        playlist.remove_tracks(tracks)?;
696        let playlist = Arc::new(playlist);
697        self.playlists.push(playlist.clone());
698        self.playlists_by_name
699            .insert(playlist.name.clone(), playlist.clone());
700        self.playlists_by_id.insert(playlist.id, playlist);
701        self.sort_playlists();
702
703        crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
704        Ok(())
705    }
706
707    // TODO: Partial importing when there are missing tracks (also should be reflected in the error
708    // type) and complete failure when the playlist is unreadable
709    // TEST: Check if importing M3U8 files works correctly
710    pub fn import_m3u8_playlist(
711        &mut self,
712        path: &PathBuf,
713        name: Option<String>,
714    ) -> Result<(), Error> {
715        trace!("Importing a playlist from an M3U8 file at {path:?}");
716        let tracks = tracks_from_m3u8(path)?;
717        let name = match name {
718            Some(n) => n,
719            None => {
720                if let Some(path) = path.file_name() {
721                    let path = path.to_string_lossy().to_string();
722                    path.strip_suffix(".m3u8")
723                        .unwrap_or(path.strip_suffix(".m3u").unwrap_or(&path))
724                        .to_string()
725                } else {
726                    self.get_default_playlist_name()
727                }
728            }
729        };
730        self.create_playlist(name, &Some(tracks))
731    }
732}
733
734#[derive(Clone, Debug, Serialize, Deserialize)]
735pub(super) struct ConfMusicLibrary {
736    playlist_id_counter: u64,
737    playlists: Vec<ConfPlaylist>,
738}
739
740impl From<MusicLibrary> for ConfMusicLibrary {
741    fn from(value: MusicLibrary) -> Self {
742        Self {
743            playlist_id_counter: value.playlist_id_counter,
744            playlists: value
745                .playlists
746                .into_iter()
747                .map(|t| t.as_ref().clone().into())
748                .collect(),
749        }
750    }
751}