1use std::error::Error;
2use std::fmt::{Display, Write as _};
3use std::fs::File;
4use std::io::{BufRead, BufReader, BufWriter, Write};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::{Context, Result, bail};
9use parking_lot::RwLock;
10use pathdiff::diff_paths;
11use rand::Rng;
12use rand::seq::SliceRandom;
13use termusiclib::config::SharedServerSettings;
14use termusiclib::config::v2::server::LoopMode;
15use termusiclib::player::PlaylistLoopModeInfo;
16use termusiclib::player::PlaylistShuffledInfo;
17use termusiclib::player::PlaylistSwapInfo;
18use termusiclib::player::PlaylistTracks;
19use termusiclib::player::UpdateEvents;
20use termusiclib::player::UpdatePlaylistEvents;
21use termusiclib::player::playlist_helpers::PlaylistPlaySpecific;
22use termusiclib::player::playlist_helpers::PlaylistSwapTrack;
23use termusiclib::player::playlist_helpers::PlaylistTrackSource;
24use termusiclib::player::playlist_helpers::{PlaylistAddTrack, PlaylistRemoveTrackIndexed};
25use termusiclib::player::{self, RunningStatus};
26use termusiclib::player::{PlaylistAddTrackInfo, PlaylistRemoveTrackInfo};
27use termusiclib::podcast::{db::Database as DBPod, episode::Episode};
28use termusiclib::track::{MediaTypes, Track, TrackData};
29use termusiclib::utils::{filetype_supported, get_app_config_path, get_parent_folder};
30
31use crate::SharedPlaylist;
32use crate::StreamTX;
33
34#[derive(Debug)]
35pub struct Playlist {
36 tracks: Vec<Track>,
38 current_track_index: usize,
40 next_track_index: Option<usize>,
44 current_track: Option<Track>,
46 status: RunningStatus,
48 loop_mode: LoopMode,
50 played_index: Vec<usize>,
52 need_proceed_to_next: bool,
54 stream_tx: StreamTX,
55
56 is_modified: bool,
58}
59
60impl Playlist {
61 pub fn new(config: &SharedServerSettings, stream_tx: StreamTX) -> Self {
63 let loop_mode = config.read().settings.player.loop_mode;
65 let current_track = None;
66
67 Self {
68 tracks: Vec::new(),
69 status: RunningStatus::Stopped,
70 loop_mode,
71 current_track_index: 0,
72 current_track,
73 played_index: Vec::new(),
74 next_track_index: None,
75 need_proceed_to_next: false,
76 stream_tx,
77 is_modified: false,
78 }
79 }
80
81 pub fn new_shared(
87 config: &SharedServerSettings,
88 stream_tx: StreamTX,
89 ) -> Result<SharedPlaylist> {
90 let mut playlist = Self::new(config, stream_tx);
91 playlist.load_apply()?;
92
93 Ok(Arc::new(RwLock::new(playlist)))
94 }
95
96 pub fn proceed(&mut self) {
98 debug!("need to proceed to next: {}", self.need_proceed_to_next);
99 self.is_modified = true;
100 if self.need_proceed_to_next {
101 self.next();
102 } else {
103 self.need_proceed_to_next = true;
104 }
105 }
106
107 pub fn proceed_false(&mut self) {
109 self.need_proceed_to_next = false;
110 }
111
112 pub fn load() -> Result<(usize, Vec<Track>)> {
122 let path = get_playlist_path()?;
123
124 let Ok(file) = File::open(&path) else {
125 File::create(&path)?;
127
128 return Ok((0, Vec::new()));
129 };
130
131 let reader = BufReader::new(file);
132 let mut lines = reader.lines();
133
134 let mut current_track_index = 0;
135 if let Some(line) = lines.next() {
136 let index_line = line?;
137 if let Ok(index) = index_line.trim().parse() {
138 current_track_index = index;
139 }
140 } else {
141 return Ok((0, Vec::new()));
143 }
144
145 let mut playlist_items = Vec::new();
146 let db_path = get_app_config_path()?;
147 let db_podcast = DBPod::new(&db_path)?;
148 let podcasts = db_podcast
149 .get_podcasts()
150 .with_context(|| "failed to get podcasts from db.")?;
151 for line in lines {
152 let line = line?;
153
154 let trimmed_line = line.trim();
155
156 if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
159 continue;
160 }
161
162 if line.starts_with("http") {
163 let mut is_podcast = false;
164 'outer: for pod in &podcasts {
165 for ep in &pod.episodes {
166 if ep.url == line.as_str() {
167 is_podcast = true;
168 let track = Track::from_podcast_episode(ep);
169 playlist_items.push(track);
170 break 'outer;
171 }
172 }
173 }
174 if !is_podcast {
175 let track = Track::new_radio(&line);
176 playlist_items.push(track);
177 }
178 continue;
179 }
180 if let Ok(track) = Track::read_track_from_path(&line) {
181 playlist_items.push(track);
182 }
183 }
184
185 let current_track_index = current_track_index.min(playlist_items.len().saturating_sub(1));
188
189 Ok((current_track_index, playlist_items))
190 }
191
192 pub fn load_apply(&mut self) -> Result<()> {
198 let (current_track_index, tracks) = Self::load()?;
199 self.current_track_index = current_track_index;
200 self.tracks = tracks;
201 self.is_modified = false;
202
203 Ok(())
204 }
205
206 pub fn load_from_grpc(&mut self, info: PlaylistTracks, podcast_db: &DBPod) -> Result<()> {
216 let current_track_index = usize::try_from(info.current_track_index)
217 .context("convert current_track_index(u64) to usize")?;
218 let mut playlist_items = Vec::with_capacity(info.tracks.len());
219
220 for (idx, track) in info.tracks.into_iter().enumerate() {
221 let at_index_usize =
222 usize::try_from(track.at_index).context("convert at_index(u64) to usize")?;
223 if idx != at_index_usize {
225 error!("Non-matching \"index\" and \"at_index\"!");
226 }
227
228 let Some(id) = track.id else {
230 bail!("Track does not have a id, which is required to load!");
231 };
232
233 let track = match PlaylistTrackSource::try_from(id)? {
234 PlaylistTrackSource::Path(v) => Track::read_track_from_path(v)?,
235 PlaylistTrackSource::Url(v) => Track::new_radio(&v),
236 PlaylistTrackSource::PodcastUrl(v) => {
237 let episode = podcast_db.get_episode_by_url(&v)?;
238 Track::from_podcast_episode(&episode)
239 }
240 };
241
242 playlist_items.push(track);
243 }
244
245 self.current_track_index = current_track_index;
246 self.tracks = playlist_items;
247 self.is_modified = true;
248
249 Ok(())
250 }
251
252 pub fn reload_tracks(&mut self) -> Result<()> {
261 let (current_track_index, tracks) = Self::load()?;
262 self.tracks = tracks;
263 self.current_track_index = current_track_index;
264 self.is_modified = false;
265
266 Ok(())
267 }
268
269 pub fn save(&mut self) -> Result<()> {
277 let path = get_playlist_path()?;
278
279 let file = File::create(&path)?;
280
281 if self.is_empty() {
283 self.is_modified = false;
284 return Ok(());
285 }
286
287 let mut writer = BufWriter::new(file);
288 writer.write_all(self.current_track_index.to_string().as_bytes())?;
289 writer.write_all(b"\n")?;
290 for track in &self.tracks {
291 let id = match track.inner() {
292 MediaTypes::Track(track_data) => track_data.path().to_string_lossy(),
293 MediaTypes::Radio(radio_track_data) => radio_track_data.url().into(),
294 MediaTypes::Podcast(podcast_track_data) => podcast_track_data.url().into(),
295 };
296 writeln!(writer, "{id}")?;
297 }
298
299 writer.flush()?;
300 self.is_modified = false;
301
302 Ok(())
303 }
304
305 pub fn save_if_modified(&mut self) -> Result<bool> {
315 if self.is_modified {
316 self.save()?;
317
318 return Ok(true);
319 }
320
321 Ok(false)
322 }
323
324 pub fn next(&mut self) {
326 self.played_index.push(self.current_track_index);
327 if let Some(index) = self.next_track_index {
330 self.current_track_index = index;
331 return;
332 }
333 self.current_track_index = self.get_next_track_index();
334 }
335
336 fn check_same_source(
342 info: &PlaylistTrackSource,
343 track_inner: &MediaTypes,
344 at_index: usize,
345 ) -> Result<()> {
346 match (info, track_inner) {
348 (PlaylistTrackSource::Path(file_url), MediaTypes::Track(id)) => {
349 if Path::new(&file_url) != id.path() {
350 bail!(
351 "Path mismatch, expected \"{file_url}\" at \"{at_index}\", found \"{}\"",
352 id.path().display()
353 );
354 }
355 }
356 (PlaylistTrackSource::Url(file_url), MediaTypes::Radio(id)) => {
357 if file_url != id.url() {
358 bail!(
359 "URI mismatch, expected \"{file_url}\" at \"{at_index}\", found \"{}\"",
360 id.url()
361 );
362 }
363 }
364 (PlaylistTrackSource::PodcastUrl(file_url), MediaTypes::Podcast(id)) => {
365 if file_url != id.url() {
366 bail!(
367 "URI mismatch, expected \"{file_url}\" at \"{at_index}\", found \"{}\"",
368 id.url()
369 );
370 }
371 }
372 (expected, got) => {
373 bail!(
374 "Type mismatch, expected \"{expected:#?}\" at \"{at_index}\" found \"{got:#?}\""
375 );
376 }
377 }
378
379 Ok(())
380 }
381
382 pub fn play_specific(&mut self, info: &PlaylistPlaySpecific) -> Result<()> {
389 let new_index =
390 usize::try_from(info.track_index).context("convert track_index(u64) to usize")?;
391
392 let Some(track_at_idx) = self.tracks.get(new_index) else {
393 bail!("Index {new_index} is out of bound {}", self.tracks.len())
394 };
395
396 Self::check_same_source(&info.id, track_at_idx.inner(), new_index)?;
397
398 self.played_index.push(self.current_track_index);
399 self.set_next_track(None);
400 self.set_current_track_index(new_index);
401 self.proceed_false();
402 self.is_modified = true;
403
404 Ok(())
405 }
406
407 fn get_next_track_index(&self) -> usize {
409 let mut next_track_index = self.current_track_index;
410 match self.loop_mode {
411 LoopMode::Single => {}
412 LoopMode::Playlist => {
413 next_track_index += 1;
414 if next_track_index >= self.len() {
415 next_track_index = 0;
416 }
417 }
418 LoopMode::Random => {
419 next_track_index = self.get_random_index();
420 }
421 }
422 next_track_index
423 }
424
425 pub fn previous(&mut self) {
429 self.set_next_track(None);
431
432 if !self.played_index.is_empty() {
433 if let Some(index) = self.played_index.pop() {
434 self.current_track_index = index;
435 self.is_modified = true;
436 return;
437 }
438 }
439 match self.loop_mode {
440 LoopMode::Single => {}
441 LoopMode::Playlist => {
442 if self.current_track_index == 0 {
443 self.current_track_index = self.len() - 1;
444 } else {
445 self.current_track_index -= 1;
446 }
447 }
448 LoopMode::Random => {
449 self.current_track_index = self.get_random_index();
450 }
451 }
452 self.is_modified = true;
453 }
454
455 #[must_use]
456 pub fn len(&self) -> usize {
457 self.tracks.len()
458 }
459
460 #[must_use]
461 pub fn is_empty(&self) -> bool {
462 self.tracks.is_empty()
463 }
464
465 pub fn swap_down(&mut self, index: usize) {
467 if index < self.len().saturating_sub(1) {
468 self.tracks.swap(index, index + 1);
469 if index == self.current_track_index {
471 self.current_track_index += 1;
472 } else if index == self.current_track_index - 1 {
473 self.current_track_index -= 1;
474 }
475 self.is_modified = true;
476 }
477 }
478
479 pub fn swap_up(&mut self, index: usize) {
481 if index > 0 {
482 self.tracks.swap(index, index - 1);
483 if index == self.current_track_index {
485 self.current_track_index -= 1;
486 } else if index == self.current_track_index + 1 {
487 self.current_track_index += 1;
488 }
489 self.is_modified = true;
490 }
491 }
492
493 pub fn swap(&mut self, index_a: usize, index_b: usize) -> Result<()> {
503 if index_a.max(index_b) >= self.tracks.len() {
505 bail!("Index {} not within tracks bounds", index_a.max(index_b));
506 }
507
508 self.tracks.swap(index_a, index_b);
509
510 let index_a = u64::try_from(index_a).unwrap();
511 let index_b = u64::try_from(index_b).unwrap();
512
513 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistSwapTracks(PlaylistSwapInfo {
514 index_a,
515 index_b,
516 }));
517 self.is_modified = true;
518
519 Ok(())
520 }
521
522 pub fn get_current_track(&mut self) -> Option<String> {
526 let mut result = None;
527 if let Some(track) = self.current_track() {
528 match track.inner() {
529 MediaTypes::Track(track_data) => {
530 result = Some(track_data.path().to_string_lossy().to_string());
531 }
532 MediaTypes::Radio(radio_track_data) => {
533 result = Some(radio_track_data.url().to_string());
534 }
535 MediaTypes::Podcast(podcast_track_data) => {
536 result = Some(podcast_track_data.url().to_string());
537 }
538 }
539 }
540 result
541 }
542
543 pub fn fetch_next_track(&mut self) -> Option<&Track> {
545 let next_index = self.get_next_track_index();
546 self.next_track_index = Some(next_index);
547 self.tracks.get(next_index)
548 }
549
550 pub fn set_status(&mut self, status: RunningStatus) {
552 self.status = status;
553 self.send_stream_ev(UpdateEvents::PlayStateChanged {
554 playing: status.as_u32(),
555 });
556 }
557
558 #[must_use]
559 pub fn is_stopped(&self) -> bool {
560 self.status == RunningStatus::Stopped
561 }
562
563 #[must_use]
564 pub fn is_paused(&self) -> bool {
565 self.status == RunningStatus::Paused
566 }
567
568 #[must_use]
569 pub fn status(&self) -> RunningStatus {
570 self.status
571 }
572
573 pub fn cycle_loop_mode(&mut self) -> LoopMode {
580 let new_mode = match self.loop_mode {
581 LoopMode::Random => LoopMode::Playlist,
582 LoopMode::Playlist => LoopMode::Single,
583 LoopMode::Single => LoopMode::Random,
584 };
585
586 self.set_loop_mode(new_mode);
587
588 self.loop_mode
589 }
590
591 pub fn set_loop_mode(&mut self, new_mode: LoopMode) {
594 if new_mode == self.loop_mode {
596 return;
597 }
598
599 self.loop_mode = new_mode;
600
601 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistLoopMode(
602 PlaylistLoopModeInfo::from(self.loop_mode),
603 ));
604 }
605
606 pub fn save_m3u(&self, filename: &Path) -> Result<()> {
613 if self.tracks.is_empty() {
614 bail!("Unable to save since the playlist is empty.");
615 }
616
617 let parent_folder = get_parent_folder(filename);
618
619 let m3u = self.get_m3u_file(&parent_folder);
620
621 std::fs::write(filename, m3u)?;
622 Ok(())
623 }
624
625 fn get_m3u_file(&self, parent_folder: &Path) -> String {
629 let mut m3u = String::from("#EXTM3U\n");
630 for track in &self.tracks {
631 let file = match track.inner() {
632 MediaTypes::Track(track_data) => {
633 let path_relative = diff_paths(track_data.path(), parent_folder);
634
635 path_relative.map_or_else(
636 || track_data.path().to_string_lossy(),
637 |v| v.to_string_lossy().to_string().into(),
638 )
639 }
640 MediaTypes::Radio(radio_track_data) => radio_track_data.url().into(),
641 MediaTypes::Podcast(podcast_track_data) => podcast_track_data.url().into(),
642 };
643
644 let _ = writeln!(m3u, "{file}");
645 }
646 m3u
647 }
648
649 pub fn add_episode(&mut self, ep: &Episode) {
655 let track = Track::from_podcast_episode(ep);
656
657 let url = track.as_podcast().unwrap().url();
658
659 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistAddTrack(
660 PlaylistAddTrackInfo {
661 at_index: u64::try_from(self.tracks.len()).unwrap(),
662 title: track.title().map(ToOwned::to_owned),
663 duration: track.duration().unwrap_or_default(),
664 trackid: PlaylistTrackSource::PodcastUrl(url.to_owned()),
666 },
667 ));
668
669 self.tracks.push(track);
670 self.is_modified = true;
671 }
672
673 pub fn add_playlist<T: AsRef<str>>(&mut self, vec: &[T]) -> Result<(), PlaylistAddErrorVec> {
679 let mut errors = PlaylistAddErrorVec::default();
680 for item in vec {
681 let Err(err) = self.add_track(item) else {
682 continue;
683 };
684 errors.push(err);
685 }
686
687 if !errors.is_empty() {
688 return Err(errors);
689 }
690
691 Ok(())
692 }
693
694 pub fn add_track<T: AsRef<str>>(&mut self, track: &T) -> Result<(), PlaylistAddError> {
703 let track_str = track.as_ref();
704 if track_str.starts_with("http") {
705 let track = Self::track_from_uri(track_str);
706 self.tracks.push(track);
707 self.is_modified = true;
708 return Ok(());
709 }
710
711 let track = Self::track_from_path(track_str)?;
712
713 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistAddTrack(
714 PlaylistAddTrackInfo {
715 at_index: u64::try_from(self.tracks.len()).unwrap(),
716 title: track.title().map(ToOwned::to_owned),
717 duration: track.duration().unwrap_or_default(),
718 trackid: PlaylistTrackSource::Path(track_str.to_string()),
719 },
720 ));
721
722 self.tracks.push(track);
723 self.is_modified = true;
724
725 Ok(())
726 }
727
728 fn source_to_track(track_location: &PlaylistTrackSource, db_pod: &DBPod) -> Result<Track> {
732 let track = match track_location {
733 PlaylistTrackSource::Path(path) => Self::track_from_path(path)?,
734 PlaylistTrackSource::Url(uri) => Self::track_from_uri(uri),
735 PlaylistTrackSource::PodcastUrl(uri) => Self::track_from_podcasturi(uri, db_pod)?,
736 };
737
738 Ok(track)
739 }
740
741 pub fn add_tracks(
753 &mut self,
754 tracks: PlaylistAddTrack,
755 db_pod: &DBPod,
756 ) -> Result<(), PlaylistAddErrorCollection> {
757 self.tracks.reserve(tracks.tracks.len());
758 let at_index = usize::try_from(tracks.at_index).unwrap();
759 let mut errors: Vec<anyhow::Error> = Vec::new();
761
762 info!(
763 "Trying to add {} tracks to the playlist",
764 tracks.tracks.len()
765 );
766
767 let mut added_tracks = 0;
768
769 if at_index >= self.len() {
770 for track_location in tracks.tracks {
772 let track = match Self::source_to_track(&track_location, db_pod) {
773 Ok(v) => v,
774 Err(err) => {
775 warn!("Error adding track: {err}");
776 errors.push(err);
777 continue;
778 }
779 };
780
781 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistAddTrack(
782 PlaylistAddTrackInfo {
783 at_index: u64::try_from(self.tracks.len()).unwrap(),
784 title: track.title().map(ToOwned::to_owned),
785 duration: track.duration().unwrap_or_default(),
786 trackid: track_location,
787 },
788 ));
789
790 self.tracks.push(track);
791 self.is_modified = true;
792 added_tracks += 1;
793 }
794 } else {
795 let mut at_index = at_index;
796 for track_location in tracks.tracks {
798 let track = match Self::source_to_track(&track_location, db_pod) {
799 Ok(v) => v,
800 Err(err) => {
801 warn!("Error adding track: {err}");
802 errors.push(err);
803 continue;
804 }
805 };
806
807 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistAddTrack(
808 PlaylistAddTrackInfo {
809 at_index: u64::try_from(at_index).unwrap(),
810 title: track.title().map(ToOwned::to_owned),
811 duration: track.duration().unwrap_or_default(),
812 trackid: track_location,
813 },
814 ));
815
816 self.tracks.insert(at_index, track);
817 self.is_modified = true;
818 at_index += 1;
819 added_tracks += 1;
820 }
821 }
822
823 info!("Added {} tracks with {} errors", added_tracks, errors.len());
824
825 if !errors.is_empty() {
826 return Err(PlaylistAddErrorCollection::from(errors));
827 }
828
829 Ok(())
830 }
831
832 pub fn remove_tracks(&mut self, tracks: PlaylistRemoveTrackIndexed) -> Result<()> {
844 let at_index = usize::try_from(tracks.at_index).unwrap();
845
846 if at_index >= self.tracks.len() {
847 bail!(
848 "at_index is higher than the length of the playlist! at_index is \"{at_index}\" and playlist length is \"{}\"",
849 self.tracks.len()
850 );
851 }
852
853 if at_index + tracks.tracks.len().saturating_sub(1) >= self.tracks.len() {
854 bail!(
855 "at_index + tracks to remove is higher than the length of the playlist! playlist length is \"{}\"",
856 self.tracks.len()
857 );
858 }
859
860 for input_track in tracks.tracks {
861 let Some(track_at_idx) = self.tracks.get(at_index) else {
863 bail!("Failed to get track at index \"{at_index}\"");
865 };
866
867 Self::check_same_source(&input_track, track_at_idx.inner(), at_index)?;
868
869 self.handle_remove(at_index);
871
872 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistRemoveTrack(
873 PlaylistRemoveTrackInfo {
874 at_index: u64::try_from(at_index).unwrap(),
875 trackid: input_track,
876 },
877 ));
878 }
879
880 Ok(())
881 }
882
883 #[allow(clippy::unnecessary_debug_formatting)] fn track_from_path(path_str: &str) -> Result<Track, PlaylistAddError> {
886 let path = Path::new(path_str);
887
888 if !filetype_supported(path) {
889 error!("unsupported filetype: {path:#?}");
890 let p = path.to_path_buf();
891 let ext = path.extension().map(|v| v.to_string_lossy().to_string());
892 return Err(PlaylistAddError::UnsupportedFileType(ext, p));
893 }
894
895 if !path.exists() {
896 return Err(PlaylistAddError::PathDoesNotExist(path.to_path_buf()));
897 }
898
899 let track = Track::read_track_from_path(path)
900 .map_err(|err| PlaylistAddError::ReadError(err, path.to_path_buf()))?;
901
902 Ok(track)
903 }
904
905 fn track_from_uri(uri: &str) -> Track {
907 Track::new_radio(uri)
908 }
909
910 fn track_from_podcasturi(uri: &str, db_pod: &DBPod) -> Result<Track> {
912 let ep = db_pod.get_episode_by_url(uri)?;
913 let track = Track::from_podcast_episode(&ep);
914
915 Ok(track)
916 }
917
918 pub fn swap_tracks(&mut self, info: &PlaylistSwapTrack) -> Result<()> {
929 let index_a =
930 usize::try_from(info.index_a).context("Failed to convert index_a to usize")?;
931 let index_b =
932 usize::try_from(info.index_b).context("Failed to convert index_b to usize")?;
933
934 self.swap(index_a, index_b)?;
935
936 Ok(())
937 }
938
939 #[must_use]
940 pub fn tracks(&self) -> &Vec<Track> {
941 &self.tracks
942 }
943
944 pub fn remove(&mut self, index: usize) {
950 let Some(track) = self.tracks.get(index) else {
951 error!("Index {index} out of bound {}", self.tracks.len());
952 return;
953 };
954
955 let track_source = track.as_track_source();
956
957 self.handle_remove(index);
958
959 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistRemoveTrack(
960 PlaylistRemoveTrackInfo {
961 at_index: u64::try_from(index).unwrap(),
962 trackid: track_source,
963 },
964 ));
965 }
966
967 fn handle_remove(&mut self, index: usize) {
969 self.tracks.remove(index);
970
971 if index <= self.current_track_index {
973 if self.current_track_index != 0 {
975 self.current_track_index -= 1;
976 }
977 }
978 }
979
980 pub fn clear(&mut self) {
983 self.tracks.clear();
984 self.played_index.clear();
985 self.next_track_index.take();
986 self.current_track_index = 0;
987 self.need_proceed_to_next = false;
988
989 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistCleared);
990 }
991
992 pub fn shuffle(&mut self) {
998 let current_track_file = self.get_current_track();
999
1000 self.tracks.shuffle(&mut rand::rng());
1001
1002 if let Some(current_track_file) = current_track_file {
1003 if let Some(index) = self.find_index_from_file(¤t_track_file) {
1004 self.current_track_index = index;
1005 }
1006 }
1007
1008 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistShuffled(
1009 PlaylistShuffledInfo {
1010 tracks: self.as_grpc_playlist_tracks().unwrap(),
1011 },
1012 ));
1013 }
1014
1015 pub fn as_grpc_playlist_tracks(&self) -> Result<PlaylistTracks> {
1022 let tracks = self
1023 .tracks()
1024 .iter()
1025 .enumerate()
1026 .map(|(idx, track)| {
1027 let at_index = u64::try_from(idx).context("track index(usize) to u64")?;
1028 let track_source = track.as_track_source();
1029
1030 Ok(player::PlaylistAddTrack {
1031 at_index,
1032 duration: Some(track.duration().unwrap_or_default().into()),
1033 id: Some(track_source.into()),
1034 optional_title: None,
1035 })
1036 })
1037 .collect::<Result<_>>()?;
1038
1039 Ok(PlaylistTracks {
1040 current_track_index: u64::try_from(self.get_current_track_index())
1041 .context("current_track_index(usize) to u64")?,
1042 tracks,
1043 })
1044 }
1045
1046 fn find_index_from_file(&self, item: &str) -> Option<usize> {
1048 for (index, track) in self.tracks.iter().enumerate() {
1049 let file = match track.inner() {
1050 MediaTypes::Track(track_data) => track_data.path().to_string_lossy(),
1051 MediaTypes::Radio(radio_track_data) => radio_track_data.url().into(),
1052 MediaTypes::Podcast(podcast_track_data) => podcast_track_data.url().into(),
1053 };
1054 if file == item {
1055 return Some(index);
1056 }
1057 }
1058 None
1059 }
1060
1061 fn get_random_index(&self) -> usize {
1063 let mut random_index = self.current_track_index;
1064
1065 if self.len() <= 1 {
1066 return 0;
1067 }
1068
1069 let mut rng = rand::rng();
1070 while self.current_track_index == random_index {
1071 random_index = rng.random_range(0..self.len());
1072 }
1073
1074 random_index
1075 }
1076
1077 pub fn remove_deleted_items(&mut self) {
1083 if let Some(current_track_file) = self.get_current_track() {
1084 let len = self.tracks.len();
1085 let old_tracks = std::mem::replace(&mut self.tracks, Vec::with_capacity(len));
1086
1087 for track in old_tracks {
1088 let Some(path) = track.as_track().map(TrackData::path) else {
1089 continue;
1090 };
1091
1092 if path.exists() {
1093 self.tracks.push(track);
1094 continue;
1095 }
1096
1097 let track_source = track.as_track_source();
1098
1099 let deleted_idx = self.tracks.len();
1104
1105 self.send_stream_ev_pl(UpdatePlaylistEvents::PlaylistRemoveTrack(
1107 PlaylistRemoveTrackInfo {
1108 at_index: u64::try_from(deleted_idx).unwrap(),
1109 trackid: track_source,
1110 },
1111 ));
1112 self.is_modified = true;
1113 }
1114
1115 match self.find_index_from_file(¤t_track_file) {
1116 Some(new_index) => self.current_track_index = new_index,
1117 None => self.current_track_index = 0,
1118 }
1119 }
1120 }
1121
1122 pub fn stop(&mut self) {
1125 self.set_status(RunningStatus::Stopped);
1126 self.set_next_track(None);
1127 self.clear_current_track();
1128 }
1129
1130 #[must_use]
1131 pub fn current_track(&self) -> Option<&Track> {
1132 if self.current_track.is_some() {
1133 return self.current_track.as_ref();
1134 }
1135 self.tracks.get(self.current_track_index)
1136 }
1137
1138 pub fn current_track_as_mut(&mut self) -> Option<&mut Track> {
1139 self.tracks.get_mut(self.current_track_index)
1140 }
1141
1142 pub fn clear_current_track(&mut self) {
1143 self.current_track = None;
1144 }
1145
1146 #[must_use]
1147 pub fn get_current_track_index(&self) -> usize {
1148 self.current_track_index
1149 }
1150
1151 pub fn set_current_track_index(&mut self, index: usize) {
1152 self.current_track_index = index;
1153 }
1154
1155 #[must_use]
1156 pub fn next_track(&self) -> Option<&Track> {
1157 let index = self.next_track_index?;
1158 self.tracks.get(index)
1159 }
1160
1161 pub fn set_next_track(&mut self, track_idx: Option<usize>) {
1162 self.next_track_index = track_idx;
1163 }
1164
1165 #[must_use]
1166 pub fn has_next_track(&self) -> bool {
1167 self.next_track_index.is_some()
1168 }
1169
1170 fn send_stream_ev_pl(&self, ev: UpdatePlaylistEvents) {
1172 if self
1174 .stream_tx
1175 .send(UpdateEvents::PlaylistChanged(ev))
1176 .is_err()
1177 {
1178 debug!("Stream Event not send: No Receivers");
1179 }
1180 }
1181
1182 fn send_stream_ev(&self, ev: UpdateEvents) {
1184 if self.stream_tx.send(ev).is_err() {
1186 debug!("Stream Event not send: No Receivers");
1187 }
1188 }
1189}
1190
1191const PLAYLIST_SAVE_FILENAME: &str = "playlist.log";
1192
1193fn get_playlist_path() -> Result<PathBuf> {
1194 let mut path = get_app_config_path()?;
1195 path.push(PLAYLIST_SAVE_FILENAME);
1196
1197 Ok(path)
1198}
1199
1200#[derive(Debug)]
1202pub struct PlaylistAddErrorCollection {
1203 pub errors: Vec<anyhow::Error>,
1204}
1205
1206impl Display for PlaylistAddErrorCollection {
1207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1208 writeln!(
1209 f,
1210 "There are {} Errors adding tracks to the playlist: [",
1211 self.errors.len()
1212 )?;
1213
1214 for err in &self.errors {
1215 writeln!(f, " {err},")?;
1216 }
1217
1218 write!(f, "]")
1219 }
1220}
1221
1222impl Error for PlaylistAddErrorCollection {}
1223
1224impl From<Vec<anyhow::Error>> for PlaylistAddErrorCollection {
1225 fn from(value: Vec<anyhow::Error>) -> Self {
1226 Self {
1227 errors: value.into_iter().map(|err| anyhow::anyhow!(err)).collect(),
1228 }
1229 }
1230}
1231
1232#[derive(Debug)]
1235pub enum PlaylistAddError {
1236 UnsupportedFileType(Option<String>, PathBuf),
1238 PathDoesNotExist(PathBuf),
1240 ReadError(anyhow::Error, PathBuf),
1243}
1244
1245impl Display for PlaylistAddError {
1246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1247 write!(
1248 f,
1249 "Failed to add to playlist because of: {}",
1250 match self {
1251 Self::UnsupportedFileType(ext, path) => {
1252 let ext = if let Some(ext) = ext {
1253 format!("Some({ext})")
1254 } else {
1255 "None".into()
1256 };
1257 format!("Unsupported File type \"{ext}\" at \"{}\"", path.display())
1258 }
1259 Self::PathDoesNotExist(path) => {
1260 format!("Path does not exist: \"{}\"", path.display())
1261 }
1262 Self::ReadError(err, path) => {
1263 format!("{err} at \"{}\"", path.display())
1264 }
1265 }
1266 )
1267 }
1268}
1269
1270impl Error for PlaylistAddError {
1271 fn source(&self) -> Option<&(dyn Error + 'static)> {
1272 if let Self::ReadError(orig, _) = self {
1273 return Some(orig.as_ref());
1274 }
1275
1276 None
1277 }
1278}
1279
1280#[derive(Debug, Default)]
1282pub struct PlaylistAddErrorVec(Vec<PlaylistAddError>);
1283
1284impl PlaylistAddErrorVec {
1285 pub fn push(&mut self, err: PlaylistAddError) {
1286 self.0.push(err);
1287 }
1288
1289 #[must_use]
1290 pub fn is_empty(&self) -> bool {
1291 self.0.is_empty()
1292 }
1293}
1294
1295impl Display for PlaylistAddErrorVec {
1296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 writeln!(f, "{} Error(s) happened:", self.0.len())?;
1298 for err in &self.0 {
1299 writeln!(f, " - {err}")?;
1300 }
1301
1302 Ok(())
1303 }
1304}
1305
1306impl Error for PlaylistAddErrorVec {}
1307
1308#[cfg(test)]
1309mod tests {
1310 use std::path::PathBuf;
1311
1312 use termusiclib::{
1313 player::playlist_helpers::PlaylistTrackSource,
1314 track::{MediaTypes, PodcastTrackData, RadioTrackData, TrackData},
1315 };
1316
1317 use super::Playlist;
1318
1319 #[test]
1320 fn should_pass_check_info() {
1321 let path = "/somewhere/file.mp3".to_string();
1322 let path2 = PathBuf::from(&path);
1323 Playlist::check_same_source(
1324 &PlaylistTrackSource::Path(path),
1325 &MediaTypes::Track(TrackData::new(path2)),
1326 0,
1327 )
1328 .unwrap();
1329
1330 let uri = "http://some.radio.com/".to_string();
1331 let uri2 = uri.clone();
1332 Playlist::check_same_source(
1333 &PlaylistTrackSource::Url(uri),
1334 &MediaTypes::Radio(RadioTrackData::new(uri2)),
1335 0,
1336 )
1337 .unwrap();
1338
1339 let uri = "http://some.podcast.com/".to_string();
1340 let uri2 = uri.clone();
1341 Playlist::check_same_source(
1342 &PlaylistTrackSource::PodcastUrl(uri),
1343 &MediaTypes::Podcast(PodcastTrackData::new(uri2)),
1344 0,
1345 )
1346 .unwrap();
1347 }
1348
1349 #[test]
1350 fn should_err_on_type_mismatch() {
1351 let path = "/somewhere/file.mp3".to_string();
1352 let path2 = path.clone();
1353 Playlist::check_same_source(
1354 &PlaylistTrackSource::Path(path),
1355 &MediaTypes::Radio(RadioTrackData::new(path2)),
1356 0,
1357 )
1358 .unwrap_err();
1359 }
1360}