use tracing::{info, span, Level, Span};
use crate::config;
use crate::songs::{Song, Songs};
use core::fmt;
use parking_lot::RwLock;
use std::sync::Arc;
#[derive(Debug, thiserror::Error)]
pub enum PlaylistError {
#[error("Song not in registry: {0}")]
SongNotFound(String),
}
pub struct Playlist {
name: String,
songs: Vec<String>,
position: Arc<RwLock<usize>>,
registry: Arc<Songs>,
span: Span,
}
impl fmt::Display for Playlist {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Playlist ({} songs):", self.songs.len())?;
for song_name in self.songs.iter() {
match self.registry.get(song_name) {
Ok(song) => writeln!(f, " - {} (Channels: {})", song.name(), song.num_channels())?,
Err(_) => writeln!(f, " - {} (unable to find song)", song_name)?,
};
}
Ok(())
}
}
impl Playlist {
pub fn new(
name: &str,
config: &config::Playlist,
registry: Arc<Songs>,
) -> Result<Arc<Playlist>, PlaylistError> {
let song_names = config.songs();
for song_name in song_names.iter() {
registry
.get(song_name)
.map_err(|_| PlaylistError::SongNotFound(song_name.clone()))?;
}
Ok(Arc::new(Playlist {
name: name.to_string(),
songs: song_names.to_vec(),
position: Arc::new(RwLock::new(0)),
registry: Arc::clone(®istry),
span: span!(Level::INFO, "playlist"),
}))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn position(&self) -> usize {
*self.position.read()
}
pub fn songs(&self) -> &Vec<String> {
&self.songs
}
pub fn next(&self) -> Option<Arc<Song>> {
let _enter = self.span.enter();
if self.songs.is_empty() {
return None;
}
let mut position = self.position.write();
if *position < self.songs.len() - 1 {
*position += 1;
}
let current = self.registry.get(&self.songs[*position]).ok()?;
info!(
position = *position,
song = current.name(),
"Moving to next playlist position."
);
Some(current)
}
pub fn prev(&self) -> Option<Arc<Song>> {
if self.songs.is_empty() {
return None;
}
let mut position = self.position.write();
if *position > 0 {
*position -= 1;
}
let current = self.registry.get(&self.songs[*position]).ok()?;
info!(
position = *position,
song = current.name(),
"Moving to previous playlist position."
);
Some(current)
}
pub fn navigate_to(&self, name: &str) -> Option<Arc<Song>> {
let idx = self.songs.iter().position(|s| s == name)?;
let song = self.registry.get(name).ok()?;
*self.position.write() = idx;
Some(song)
}
pub fn registry(&self) -> &Arc<Songs> {
&self.registry
}
pub fn get_song(&self, name: &str) -> Option<Arc<Song>> {
self.registry.get(name).ok()
}
pub fn current(&self) -> Option<Arc<Song>> {
if self.songs.is_empty() {
return None;
}
let position = self.position.read();
self.registry.get(&self.songs[*position]).ok()
}
}
pub fn from_songs(songs: Arc<Songs>) -> Result<Arc<Playlist>, PlaylistError> {
let sorted = Vec::from_iter(
songs
.sorted_list()
.into_iter()
.map(|song| song.name().to_string()),
);
Playlist::new("all_songs", &config::Playlist::new(&sorted), songs)
}
#[cfg(test)]
mod test {
use std::path::Path;
use std::sync::Arc;
use crate::{config, songs, songs::Songs};
fn test_registry() -> Arc<Songs> {
songs::get_all_songs(Path::new("assets/songs")).expect("Parse songs should have succeeded.")
}
fn two_song_playlist(registry: Arc<Songs>) -> Arc<super::Playlist> {
super::Playlist::new(
"Test Playlist",
&config::Playlist::new(&["Song 1".to_string(), "Song 2".to_string()]),
registry,
)
.expect("Unable to create playlist")
}
#[test]
fn test_playlist() {
let playlist = two_song_playlist(test_registry());
assert_eq!("Song 1", playlist.current().unwrap().name());
playlist.prev();
assert_eq!("Song 1", playlist.current().unwrap().name());
playlist.next();
assert_eq!("Song 2", playlist.current().unwrap().name());
playlist.next();
assert_eq!("Song 2", playlist.current().unwrap().name());
playlist.prev();
assert_eq!("Song 1", playlist.current().unwrap().name());
}
#[test]
fn position_tracking() {
let playlist = two_song_playlist(test_registry());
assert_eq!(playlist.position(), 0);
playlist.next();
assert_eq!(playlist.position(), 1);
playlist.prev();
assert_eq!(playlist.position(), 0);
}
#[test]
fn empty_playlist() {
let registry = test_registry();
let playlist = super::Playlist {
name: "empty".to_string(),
songs: vec![],
position: Arc::new(parking_lot::RwLock::new(0)),
registry,
span: tracing::span!(tracing::Level::INFO, "test"),
};
assert!(playlist.current().is_none());
assert!(playlist.next().is_none());
assert!(playlist.prev().is_none());
}
#[test]
fn name() {
let playlist = two_song_playlist(test_registry());
assert_eq!(playlist.name(), "Test Playlist");
}
#[test]
fn songs_list() {
let playlist = two_song_playlist(test_registry());
assert_eq!(playlist.songs(), &["Song 1", "Song 2"]);
}
#[test]
fn get_song_found() {
let playlist = two_song_playlist(test_registry());
let song = playlist.get_song("Song 1");
assert!(song.is_some());
assert_eq!(song.unwrap().name(), "Song 1");
}
#[test]
fn get_song_not_found() {
let playlist = two_song_playlist(test_registry());
assert!(playlist.get_song("Nonexistent Song").is_none());
}
#[test]
fn song_not_in_registry_error() {
let registry = test_registry();
let result = super::Playlist::new(
"Bad Playlist",
&config::Playlist::new(&["Song 1".to_string(), "No Such Song".to_string()]),
registry,
);
let err = result.err().expect("should be an error");
assert!(
err.to_string().contains("No Such Song"),
"error should mention missing song name: {}",
err
);
}
#[test]
fn from_songs_all_songs_playlist() {
let registry = test_registry();
let all = super::from_songs(Arc::clone(®istry)).expect("from_songs");
assert_eq!(all.name(), "all_songs");
let names: Vec<&str> = all.songs().iter().map(|s| s.as_str()).collect();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(names, sorted);
assert!(!names.is_empty());
}
#[test]
fn next_returns_correct_song() {
let playlist = two_song_playlist(test_registry());
let song = playlist.next().unwrap();
assert_eq!(song.name(), "Song 2");
}
#[test]
fn prev_returns_correct_song() {
let playlist = two_song_playlist(test_registry());
playlist.next(); let song = playlist.prev().unwrap();
assert_eq!(song.name(), "Song 1");
}
#[test]
fn display_impl() {
let playlist = two_song_playlist(test_registry());
let display = format!("{}", playlist);
assert!(display.contains("Playlist (2 songs):"));
assert!(display.contains("Song 1"));
assert!(display.contains("Song 2"));
}
#[test]
fn navigate_to_found() {
let playlist = two_song_playlist(test_registry());
assert_eq!(playlist.position(), 0);
let song = playlist.navigate_to("Song 2");
assert!(song.is_some());
assert_eq!(song.unwrap().name(), "Song 2");
assert_eq!(playlist.position(), 1);
assert_eq!(playlist.current().unwrap().name(), "Song 2");
}
#[test]
fn navigate_to_not_found() {
let playlist = two_song_playlist(test_registry());
let song = playlist.navigate_to("Nonexistent");
assert!(song.is_none());
assert_eq!(playlist.position(), 0);
}
#[test]
fn navigate_to_first_song() {
let playlist = two_song_playlist(test_registry());
playlist.next(); assert_eq!(playlist.position(), 1);
let song = playlist.navigate_to("Song 1");
assert!(song.is_some());
assert_eq!(playlist.position(), 0);
}
#[test]
fn display_with_missing_song_shows_error() {
let registry = test_registry();
let playlist = super::Playlist {
name: "broken".to_string(),
songs: vec!["Song 1".to_string(), "Ghost Song".to_string()],
position: Arc::new(parking_lot::RwLock::new(0)),
registry,
span: tracing::span!(tracing::Level::INFO, "test"),
};
let display = format!("{}", playlist);
assert!(
display.contains("unable to find song"),
"display should show error for missing song: {}",
display
);
assert!(display.contains("Ghost Song"));
}
}