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
83impl 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 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 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 pub fn tracks_from_hashes(&self, hashes: Vec<u64>) -> HashMatchingResult {
523 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 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}