termusicplayback/
playlist.rs

1use anyhow::{bail, Context, Result};
2use pathdiff::diff_paths;
3use rand::seq::SliceRandom;
4use rand::thread_rng;
5use rand::Rng;
6use std::error::Error;
7use std::fmt::Display;
8use std::fs::File;
9use std::io::{BufRead, BufReader, BufWriter, Write};
10use std::path::{Path, PathBuf};
11use termusiclib::config::v2::server::LoopMode;
12use termusiclib::config::SharedServerSettings;
13use termusiclib::podcast::{db::Database as DBPod, episode::Episode};
14use termusiclib::track::MediaType;
15use termusiclib::{
16    track::Track,
17    utils::{filetype_supported, get_app_config_path, get_parent_folder},
18};
19
20#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
21pub enum Status {
22    #[default]
23    Stopped,
24    Running,
25    Paused,
26}
27
28impl Status {
29    #[must_use]
30    pub fn as_u32(&self) -> u32 {
31        match self {
32            Status::Stopped => 0,
33            Status::Running => 1,
34            Status::Paused => 2,
35        }
36    }
37
38    #[must_use]
39    pub fn from_u32(status: u32) -> Self {
40        match status {
41            1 => Status::Running,
42            2 => Status::Paused,
43            _ => Status::Stopped,
44        }
45    }
46}
47
48impl std::fmt::Display for Status {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::Running => write!(f, "Running"),
52            Self::Stopped => write!(f, "Stopped"),
53            Self::Paused => write!(f, "Paused"),
54        }
55    }
56}
57
58#[derive(Debug)]
59pub struct Playlist {
60    /// All tracks in the playlist
61    tracks: Vec<Track>,
62    /// Index into `tracks` of which the current playing track is
63    current_track_index: usize,
64    /// Index into `tracks` for the next track to play after the current
65    next_track_index: Option<usize>,
66    /// The currently playing [`Track`]. Does not need to be in `tracks`
67    current_track: Option<Track>,
68    /// The current playing running status of the playlist
69    status: Status,
70    /// The loop-/play-mode for the playlist
71    loop_mode: LoopMode,
72    /// Indexes into `tracks` that have been previously been played (for `previous`)
73    played_index: Vec<usize>,
74    /// Indicator if the playlist should advance the `current_*` and `next_*` values
75    need_proceed_to_next: bool,
76}
77
78impl Playlist {
79    /// # Errors
80    /// errors could happen when reading files
81    pub fn new(config: &SharedServerSettings) -> Result<Self> {
82        let (current_track_index, tracks) = Self::load()?;
83        // TODO: shouldnt "loop_mode" be combined with the config ones?
84        let loop_mode = config.read().settings.player.loop_mode;
85        let current_track = None;
86
87        Ok(Self {
88            tracks,
89            status: Status::Stopped,
90            loop_mode,
91            current_track_index,
92            current_track,
93            played_index: Vec::new(),
94            next_track_index: None,
95            need_proceed_to_next: false,
96        })
97    }
98
99    /// Advance the playlist to the next track.
100    pub fn proceed(&mut self) {
101        debug!("need to proceed to next: {}", self.need_proceed_to_next);
102        if self.need_proceed_to_next {
103            self.next();
104        } else {
105            self.need_proceed_to_next = true;
106        }
107    }
108
109    /// Set `need_proceed_to_next` to `false`
110    pub fn proceed_false(&mut self) {
111        self.need_proceed_to_next = false;
112    }
113
114    /// Load the playlist from the file.
115    ///
116    /// Path in `$config$/playlist.log`.
117    ///
118    /// Returns `(Position, Tracks[])`.
119    ///
120    /// # Errors
121    /// - When the playlist path is not write-able
122    /// - When podcasts cannot be loaded
123    pub fn load() -> Result<(usize, Vec<Track>)> {
124        let path = get_playlist_path()?;
125
126        let Ok(file) = File::open(&path) else {
127            // new file, nothing to parse from it
128            File::create(&path)?;
129
130            return Ok((0, Vec::new()));
131        };
132
133        let reader = BufReader::new(file);
134        let mut lines = reader.lines();
135
136        let mut current_track_index = 0;
137        if let Some(line) = lines.next() {
138            let index_line = line?;
139            if let Ok(index) = index_line.trim().parse() {
140                current_track_index = index;
141            }
142        } else {
143            // empty file, nothing to parse from it
144            return Ok((0, Vec::new()));
145        }
146
147        let mut playlist_items = Vec::new();
148        let db_path = get_app_config_path()?;
149        let db_podcast = DBPod::new(&db_path)?;
150        let podcasts = db_podcast
151            .get_podcasts()
152            .with_context(|| "failed to get podcasts from db.")?;
153        for line in lines {
154            let line = line?;
155            if line.starts_with("http") {
156                let mut is_podcast = false;
157                'outer: for pod in &podcasts {
158                    for ep in &pod.episodes {
159                        if ep.url == line.as_str() {
160                            is_podcast = true;
161                            let track = Track::from_episode(ep);
162                            playlist_items.push(track);
163                            break 'outer;
164                        }
165                    }
166                }
167                if !is_podcast {
168                    let track = Track::new_radio(&line);
169                    playlist_items.push(track);
170                }
171                continue;
172            }
173            if let Ok(track) = Track::read_from_path(&line, false) {
174                playlist_items.push(track);
175                continue;
176            };
177        }
178
179        Ok((current_track_index, playlist_items))
180    }
181
182    /// Reload the current playlist from the file. This function does not save beforehand.
183    ///
184    /// # Errors
185    /// See [`Self::load`]
186    pub fn reload_tracks(&mut self) -> Result<()> {
187        let (current_track_index, tracks) = Self::load()?;
188        self.tracks = tracks;
189        self.current_track_index = current_track_index;
190        Ok(())
191    }
192
193    /// Save the current playlist and playing index to the playlist log
194    ///
195    /// Path in `$config$/playlist.log`
196    ///
197    /// # Errors
198    /// Errors could happen when writing files
199    pub fn save(&mut self) -> Result<()> {
200        let path = get_playlist_path()?;
201
202        let file = File::create(&path)?;
203
204        // If the playlist is empty, truncate the file, but dont write anything else (like a index number)
205        if self.is_empty() {
206            return Ok(());
207        }
208
209        let mut writer = BufWriter::new(file);
210        writer.write_all(self.current_track_index.to_string().as_bytes())?;
211        writer.write_all(b"\n")?;
212        for track in &self.tracks {
213            if let Some(f) = track.file() {
214                writer.write_all(f.as_bytes())?;
215                writer.write_all(b"\n")?;
216            }
217        }
218
219        writer.flush()?;
220
221        Ok(())
222    }
223
224    /// Change to the next track.
225    pub fn next(&mut self) {
226        self.played_index.push(self.current_track_index);
227        // Note: the next index is *not* taken here, as ".proceed/next" is called first,
228        // then "has_next_track" is later used to check if enqueing has used.
229        if let Some(index) = self.next_track_index {
230            self.current_track_index = index;
231            return;
232        }
233        self.current_track_index = self.get_next_track_index();
234    }
235
236    /// Get the next track index based on the [`LoopMode`] used.
237    fn get_next_track_index(&self) -> usize {
238        let mut next_track_index = self.current_track_index;
239        match self.loop_mode {
240            LoopMode::Single => {}
241            LoopMode::Playlist => {
242                next_track_index += 1;
243                if next_track_index >= self.len() {
244                    next_track_index = 0;
245                }
246            }
247            LoopMode::Random => {
248                next_track_index = self.get_random_index();
249            }
250        }
251        next_track_index
252    }
253
254    /// Change to the previous track played.
255    ///
256    /// This uses `played_index` vec, if available, otherwise uses [`LoopMode`].
257    pub fn previous(&mut self) {
258        if !self.played_index.is_empty() {
259            if let Some(index) = self.played_index.pop() {
260                self.current_track_index = index;
261                return;
262            }
263        }
264        match self.loop_mode {
265            LoopMode::Single => {}
266            LoopMode::Playlist => {
267                if self.current_track_index == 0 {
268                    self.current_track_index = self.len() - 1;
269                } else {
270                    self.current_track_index -= 1;
271                }
272            }
273            LoopMode::Random => {
274                self.current_track_index = self.get_random_index();
275            }
276        }
277    }
278
279    #[must_use]
280    pub fn len(&self) -> usize {
281        self.tracks.len()
282    }
283
284    #[must_use]
285    pub fn is_empty(&self) -> bool {
286        self.tracks.is_empty()
287    }
288
289    /// Swap the `index` with the one below(+1) it, if there is one.
290    pub fn swap_down(&mut self, index: usize) {
291        if index < self.len().saturating_sub(1) {
292            self.tracks.swap(index, index + 1);
293            // handle index
294            if index == self.current_track_index {
295                self.current_track_index += 1;
296            } else if index == self.current_track_index - 1 {
297                self.current_track_index -= 1;
298            }
299        }
300    }
301
302    /// Swap the `index` with the one above(-1) it, if there is one.
303    pub fn swap_up(&mut self, index: usize) {
304        if index > 0 {
305            self.tracks.swap(index, index - 1);
306            // handle index
307            if index == self.current_track_index {
308                self.current_track_index -= 1;
309            } else if index == self.current_track_index + 1 {
310                self.current_track_index += 1;
311            }
312        }
313    }
314
315    /// Get the current track's Path/Url.
316    pub fn get_current_track(&mut self) -> Option<String> {
317        let mut result = None;
318        if let Some(track) = self.current_track() {
319            match track.media_type {
320                MediaType::Music | MediaType::LiveRadio => {
321                    if let Some(file) = track.file() {
322                        result = Some(file.to_string());
323                    }
324                }
325                MediaType::Podcast => {
326                    if let Some(local_file) = &track.podcast_localfile {
327                        let path = Path::new(&local_file);
328                        if path.exists() {
329                            return Some(local_file.clone());
330                        }
331                    }
332                    if let Some(file) = track.file() {
333                        result = Some(file.to_string());
334                    }
335                }
336            }
337        }
338        result
339    }
340
341    /// Get the next track index and return a reference to it.
342    pub fn fetch_next_track(&mut self) -> Option<&Track> {
343        let next_index = self.get_next_track_index();
344        self.next_track_index = Some(next_index);
345        self.tracks.get(next_index)
346    }
347
348    pub fn set_status(&mut self, status: Status) {
349        self.status = status;
350    }
351
352    #[must_use]
353    pub fn is_stopped(&self) -> bool {
354        self.status == Status::Stopped
355    }
356
357    #[must_use]
358    pub fn is_paused(&self) -> bool {
359        self.status == Status::Paused
360    }
361
362    #[must_use]
363    pub fn status(&self) -> Status {
364        self.status
365    }
366
367    /// Cycle through the loop modes and return the new mode.
368    ///
369    /// order:
370    /// [Random](LoopMode::Random) -> [Playlist](LoopMode::Playlist)
371    /// [Playlist](LoopMode::Playlist) -> [Single](LoopMode::Single)
372    /// [Single](LoopMode::Single) -> [Random](LoopMode::Random)
373    pub fn cycle_loop_mode(&mut self) -> LoopMode {
374        match self.loop_mode {
375            LoopMode::Random => {
376                self.loop_mode = LoopMode::Playlist;
377            }
378            LoopMode::Playlist => {
379                self.loop_mode = LoopMode::Single;
380            }
381            LoopMode::Single => {
382                self.loop_mode = LoopMode::Random;
383            }
384        };
385        self.loop_mode
386    }
387
388    /// Export the current playlist to a `.m3u` playlist file.
389    ///
390    /// Might be confused with [save](Self::save).
391    ///
392    /// # Errors
393    /// Error could happen when writing file to local disk.
394    pub fn save_m3u(&self, filename: &Path) -> Result<()> {
395        if self.tracks.is_empty() {
396            bail!("Unable to save since the playlist is empty.");
397        }
398
399        let parent_folder = get_parent_folder(filename);
400
401        let m3u = self.get_m3u_file(&parent_folder);
402
403        std::fs::write(filename, m3u)?;
404        Ok(())
405    }
406
407    /// Generate the m3u's file content.
408    ///
409    /// All Paths are relative to the `parent_folder` directory.
410    fn get_m3u_file(&self, parent_folder: &Path) -> String {
411        let mut m3u = String::from("#EXTM3U\n");
412        for track in &self.tracks {
413            if let Some(file) = track.file() {
414                let path_relative = diff_paths(file, parent_folder);
415
416                if let Some(path_relative) = path_relative {
417                    let path = format!("{}\n", path_relative.display());
418                    m3u.push_str(&path);
419                }
420            }
421        }
422        m3u
423    }
424
425    /// Add a podcast episode to the playlist.
426    pub fn add_episode(&mut self, ep: &Episode) {
427        let track = Track::from_episode(ep);
428        self.tracks.push(track);
429    }
430
431    /// Add many Paths/Urls to the playlist.
432    ///
433    /// # Errors
434    /// - When invalid inputs are given
435    /// - When the file(s) cannot be read correctly
436    pub fn add_playlist<T: AsRef<str>>(&mut self, vec: &[T]) -> Result<(), PlaylistAddErrorVec> {
437        let mut errors = PlaylistAddErrorVec::default();
438        for item in vec {
439            let Err(err) = self.add_track(item) else {
440                continue;
441            };
442            errors.push(err);
443        }
444
445        if !errors.is_empty() {
446            return Err(errors);
447        }
448
449        Ok(())
450    }
451
452    /// Add a single Path/Url to the playlist
453    ///
454    /// # Errors
455    /// - When invalid inputs are given (non-existing path, unsupported file types, etc)
456    pub fn add_track<T: AsRef<str>>(&mut self, track: &T) -> Result<(), PlaylistAddError> {
457        let track = track.as_ref();
458        if track.starts_with("http") {
459            let track = Track::new_radio(track);
460            self.tracks.push(track);
461            return Ok(());
462        }
463        let path = Path::new(track);
464        if !filetype_supported(track) {
465            error!("unsupported filetype: {:#?}", track);
466            let p = path.to_path_buf();
467            let ext = p.extension().map(|v| v.to_string_lossy().to_string());
468            return Err(PlaylistAddError::UnsupportedFileType(ext, p));
469        }
470        if !path.exists() {
471            return Err(PlaylistAddError::PathDoesNotExist(path.to_path_buf()));
472        }
473
474        let track = Track::read_from_path(track, false)
475            .map_err(|err| PlaylistAddError::ReadError(err, path.to_path_buf()))?;
476
477        self.tracks.push(track);
478
479        Ok(())
480    }
481
482    #[must_use]
483    pub fn tracks(&self) -> &Vec<Track> {
484        &self.tracks
485    }
486
487    /// Remove the track at `index`. Does not modify `current_track`.
488    pub fn remove(&mut self, index: usize) {
489        self.tracks.remove(index);
490        // Handle index
491        if index <= self.current_track_index {
492            // nothing needs to be done if the index is already 0
493            if self.current_track_index != 0 {
494                self.current_track_index -= 1;
495            }
496        }
497    }
498
499    /// Clear the current playlist.
500    /// This does not stop the playlist or clear [`current_track`].
501    pub fn clear(&mut self) {
502        self.tracks.clear();
503        self.played_index.clear();
504        self.next_track_index.take();
505        self.current_track_index = 0;
506        self.need_proceed_to_next = false;
507    }
508
509    /// Shuffle the playlist
510    pub fn shuffle(&mut self) {
511        // TODO: why does this only shuffle if there is a current track?
512        if let Some(current_track_file) = self.get_current_track() {
513            self.tracks.shuffle(&mut thread_rng());
514            if let Some(index) = self.find_index_from_file(&current_track_file) {
515                self.current_track_index = index;
516            }
517        }
518    }
519
520    /// Find the index in the playlist for `item`, if it exists there.
521    fn find_index_from_file(&self, item: &str) -> Option<usize> {
522        for (index, track) in self.tracks.iter().enumerate() {
523            let Some(file) = track.file() else {
524                continue;
525            };
526            if file == item {
527                return Some(index);
528            }
529        }
530        None
531    }
532
533    /// Get a random index in the playlist.
534    fn get_random_index(&self) -> usize {
535        let mut random_index = self.current_track_index;
536
537        if self.len() <= 1 {
538            return 0;
539        }
540
541        let mut rng = rand::thread_rng();
542        while self.current_track_index == random_index {
543            random_index = rng.gen_range(0..self.len());
544        }
545
546        random_index
547    }
548
549    /// Remove all tracks from the playlist that dont exist on the disk.
550    pub fn remove_deleted_items(&mut self) {
551        if let Some(current_track_file) = self.get_current_track() {
552            // TODO: dosnt this remove radio and podcast episodes?
553            self.tracks
554                .retain(|x| x.file().is_some_and(|p| Path::new(p).exists()));
555            match self.find_index_from_file(&current_track_file) {
556                Some(new_index) => self.current_track_index = new_index,
557                None => self.current_track_index = 0,
558            }
559        }
560    }
561
562    /// Stop the current playlist by setting [`Status::Stopped`], preventing going to the next track
563    /// and finally, stop the currently playing track.
564    pub fn stop(&mut self) {
565        self.set_status(Status::Stopped);
566        self.set_next_track(None);
567        self.clear_current_track();
568    }
569
570    #[must_use]
571    pub fn current_track(&self) -> Option<&Track> {
572        if self.current_track.is_some() {
573            return self.current_track.as_ref();
574        }
575        self.tracks.get(self.current_track_index)
576    }
577
578    pub fn current_track_as_mut(&mut self) -> Option<&mut Track> {
579        self.tracks.get_mut(self.current_track_index)
580    }
581
582    pub fn clear_current_track(&mut self) {
583        self.current_track = None;
584    }
585
586    #[must_use]
587    pub fn get_current_track_index(&self) -> usize {
588        self.current_track_index
589    }
590
591    pub fn set_current_track_index(&mut self, index: usize) {
592        self.current_track_index = index;
593    }
594
595    #[must_use]
596    pub fn next_track(&self) -> Option<&Track> {
597        let index = self.next_track_index?;
598        self.tracks.get(index)
599    }
600
601    pub fn set_next_track(&mut self, track_idx: Option<usize>) {
602        self.next_track_index = track_idx;
603    }
604
605    #[must_use]
606    pub fn has_next_track(&self) -> bool {
607        self.next_track_index.is_some()
608    }
609}
610
611const PLAYLIST_SAVE_FILENAME: &str = "playlist.log";
612
613fn get_playlist_path() -> Result<PathBuf> {
614    let mut path = get_app_config_path()?;
615    path.push(PLAYLIST_SAVE_FILENAME);
616
617    Ok(path)
618}
619
620// NOTE: this is not "thiserror" due to custom "Display" impl (the "Option" handling)
621/// Error for when [`Playlist::add_track`] fails
622#[derive(Debug)]
623pub enum PlaylistAddError {
624    /// `(FileType, Path)`
625    UnsupportedFileType(Option<String>, PathBuf),
626    /// `(Path)`
627    PathDoesNotExist(PathBuf),
628    /// Generic Error for when reading the track fails
629    /// `(OriginalError, Path)`
630    ReadError(anyhow::Error, PathBuf),
631}
632
633impl Display for PlaylistAddError {
634    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
635        write!(
636            f,
637            "Failed to add to playlist because of: {}",
638            match self {
639                Self::UnsupportedFileType(ext, path) => {
640                    let ext = if let Some(ext) = ext {
641                        format!("Some({ext})")
642                    } else {
643                        "None".into()
644                    };
645                    format!("Unsupported File type \"{ext}\" at \"{}\"", path.display())
646                }
647                Self::PathDoesNotExist(path) => {
648                    format!("Path does not exist: \"{}\"", path.display())
649                }
650                Self::ReadError(err, path) => {
651                    format!("{err} at \"{}\"", path.display())
652                }
653            }
654        )
655    }
656}
657
658impl Error for PlaylistAddError {
659    fn source(&self) -> Option<&(dyn Error + 'static)> {
660        if let Self::ReadError(orig, _) = self {
661            return Some(orig.as_ref());
662        }
663
664        None
665    }
666}
667
668/// Error for when [`Playlist::add_playlist`] fails
669#[derive(Debug, Default)]
670pub struct PlaylistAddErrorVec(Vec<PlaylistAddError>);
671
672impl PlaylistAddErrorVec {
673    pub fn push(&mut self, err: PlaylistAddError) {
674        self.0.push(err);
675    }
676
677    #[must_use]
678    pub fn is_empty(&self) -> bool {
679        self.0.is_empty()
680    }
681}
682
683impl Display for PlaylistAddErrorVec {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        writeln!(f, "{} Error(s) happened:", self.0.len())?;
686        for err in &self.0 {
687            writeln!(f, "  - {err}")?;
688        }
689
690        Ok(())
691    }
692}
693
694impl Error for PlaylistAddErrorVec {}