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 tracks: Vec<Track>,
62 current_track_index: usize,
64 next_track_index: Option<usize>,
66 current_track: Option<Track>,
68 status: Status,
70 loop_mode: LoopMode,
72 played_index: Vec<usize>,
74 need_proceed_to_next: bool,
76}
77
78impl Playlist {
79 pub fn new(config: &SharedServerSettings) -> Result<Self> {
82 let (current_track_index, tracks) = Self::load()?;
83 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 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 pub fn proceed_false(&mut self) {
111 self.need_proceed_to_next = false;
112 }
113
114 pub fn load() -> Result<(usize, Vec<Track>)> {
124 let path = get_playlist_path()?;
125
126 let Ok(file) = File::open(&path) else {
127 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 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 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 pub fn save(&mut self) -> Result<()> {
200 let path = get_playlist_path()?;
201
202 let file = File::create(&path)?;
203
204 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 pub fn next(&mut self) {
226 self.played_index.push(self.current_track_index);
227 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 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 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 pub fn swap_down(&mut self, index: usize) {
291 if index < self.len().saturating_sub(1) {
292 self.tracks.swap(index, index + 1);
293 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 pub fn swap_up(&mut self, index: usize) {
304 if index > 0 {
305 self.tracks.swap(index, index - 1);
306 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 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 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 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 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 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 pub fn add_episode(&mut self, ep: &Episode) {
427 let track = Track::from_episode(ep);
428 self.tracks.push(track);
429 }
430
431 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 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 pub fn remove(&mut self, index: usize) {
489 self.tracks.remove(index);
490 if index <= self.current_track_index {
492 if self.current_track_index != 0 {
494 self.current_track_index -= 1;
495 }
496 }
497 }
498
499 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 pub fn shuffle(&mut self) {
511 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(¤t_track_file) {
515 self.current_track_index = index;
516 }
517 }
518 }
519
520 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 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 pub fn remove_deleted_items(&mut self) {
551 if let Some(current_track_file) = self.get_current_track() {
552 self.tracks
554 .retain(|x| x.file().is_some_and(|p| Path::new(p).exists()));
555 match self.find_index_from_file(¤t_track_file) {
556 Some(new_index) => self.current_track_index = new_index,
557 None => self.current_track_index = 0,
558 }
559 }
560 }
561
562 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#[derive(Debug)]
623pub enum PlaylistAddError {
624 UnsupportedFileType(Option<String>, PathBuf),
626 PathDoesNotExist(PathBuf),
628 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#[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 {}