#[cfg(test)]
use std::collections::HashMap;
use std::collections::VecDeque;
use serde::{Deserialize, Serialize};
pub use crate::providers::ProviderTrack;
use crate::providers::{ProviderId, ProviderMap};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrackAlbum {
pub name: String,
pub id: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Track {
pub title: String,
pub artist: String,
pub source: ProviderId,
pub providers: ProviderMap,
}
impl Track {
fn source_provider(&self) -> Option<&ProviderTrack> {
if let Some(pt) = self.providers.get(&self.source) {
return Some(pt);
}
self.providers.values().next()
}
pub fn duration(&self) -> u32 {
self.source_provider()
.map(|p| p.duration)
.or_else(|| {
self.providers
.values()
.find_map(|p| (p.duration > 0).then_some(p.duration))
})
.unwrap_or(0)
}
pub fn thumbnail(&self) -> &str {
self.source_provider()
.and_then(|p| (!p.thumbnail.is_empty()).then_some(p.thumbnail.as_str()))
.or_else(|| {
self.providers
.values()
.find_map(|p| (!p.thumbnail.is_empty()).then_some(p.thumbnail.as_str()))
})
.unwrap_or("")
}
pub fn play_count(&self) -> u64 {
self.source_provider().map_or(0, |p| p.play_count).max(
self.providers
.values()
.map(|p| p.play_count)
.max()
.unwrap_or(0),
)
}
pub fn album(&self) -> Option<&TrackAlbum> {
self.source_provider()
.and_then(|p| p.album.as_ref())
.or_else(|| self.providers.values().find_map(|p| p.album.as_ref()))
}
}
impl Track {
pub fn provider_id(&self, provider: ProviderId) -> Option<&str> {
self.providers.get(&provider).map(|t| t.id.as_str())
}
pub fn provider_url(&self, provider: ProviderId) -> Option<&str> {
self.providers.get(&provider).map(|t| t.url.as_str())
}
pub fn provider_artist_id(&self, provider: ProviderId) -> Option<&str> {
self.providers
.get(&provider)
.and_then(|t| t.artist_id.as_deref())
}
pub fn has_provider(&self, provider: ProviderId) -> bool {
self.providers.contains_key(&provider)
}
pub fn local_path(&self) -> Option<String> {
let local = self.providers.get(&ProviderId::Local)?;
(!local.url.is_empty()).then(|| local.url.clone())
}
pub fn download_path(&self) -> Option<String> {
self.local_path()
}
pub fn set_download_path(&mut self, path: String) {
self.providers.entry(ProviderId::Local).or_default().url = path;
}
pub fn set_provider(&mut self, provider: ProviderId, pt: ProviderTrack) {
self.providers.insert(provider, pt);
if self.source == ProviderId::Local && provider != ProviderId::Local {
self.source = provider;
}
}
pub fn can_download_from(&self, provider: ProviderId) -> bool {
provider.capabilities().download && self.providers.contains_key(&provider)
}
pub fn best_stream_provider(&self, preferred: ProviderId) -> Option<ProviderId> {
let candidates: Vec<ProviderId> = self
.providers
.keys()
.copied()
.filter(|p| p.capabilities().stream && p.capabilities().download)
.collect();
if candidates.contains(&preferred) {
return Some(preferred);
}
if candidates.contains(&self.source) {
return Some(self.source);
}
candidates.first().copied()
}
pub fn dedup_key(&self) -> String {
format!("{}|{}", self.title, self.artist)
}
pub fn search_query(&self) -> String {
if self.artist.is_empty() {
self.title.clone()
} else {
format!("{} {}", self.title, self.artist)
}
}
pub fn primary_id(&self) -> &str {
self.provider_id(self.source).unwrap_or("")
}
pub fn primary_url(&self) -> &str {
self.provider_url(self.source).unwrap_or("")
}
pub fn cache_key(&self) -> String {
let id = self.primary_id();
format!("{:?}:{}", self.source, id)
}
#[allow(clippy::too_many_arguments)]
pub fn from_provider(
provider: ProviderId,
id: String,
url: String,
title: impl Into<String>,
artist_name: impl Into<String>,
duration: u32,
thumbnail: impl Into<String>,
album: Option<TrackAlbum>,
artist_id: Option<String>,
) -> Self {
let mut providers = ProviderMap::new();
providers.insert(
provider,
ProviderTrack {
id,
url,
artist_id,
duration,
thumbnail: thumbnail.into(),
album,
play_count: 0,
},
);
Self {
title: title.into(),
artist: artist_name.into(),
source: provider,
providers,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum QueueTab {
#[default]
Queue,
RecentlyPlayed,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PlayQueue {
pub tracks: Vec<Track>,
pub recently_played: VecDeque<Track>,
pub queue_tab: QueueTab,
}
impl PlayQueue {
pub const fn new() -> Self {
Self {
tracks: Vec::new(),
recently_played: VecDeque::new(),
queue_tab: QueueTab::Queue,
}
}
pub fn current(&self) -> Option<&Track> {
self.tracks.first()
}
pub fn advance(&mut self) -> bool {
if self.tracks.is_empty() {
false
} else {
self.tracks.remove(0);
true
}
}
pub fn record_played(&mut self, track: &Track, max_len: usize) {
let key = track.dedup_key();
self.recently_played.retain(|t| t.dedup_key() != key);
self.recently_played.push_front(track.clone());
while self.recently_played.len() > max_len {
self.recently_played.pop_back();
}
}
pub fn restore_previous(&mut self) -> bool {
if let Some(track) = self.recently_played.pop_front() {
self.tracks.insert(0, track);
true
} else {
false
}
}
pub fn set_queue(&mut self, tracks: Vec<Track>, max_len: usize) {
let old = self.current().cloned();
let new_key = tracks.first().map(Track::cache_key);
if let (Some(old), Some(new_key)) = (old, new_key) {
if old.cache_key() != new_key {
self.record_played(&old, max_len);
}
}
self.tracks = tracks;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_track(id: &str, url: &str) -> Track {
let mut t = Track {
title: format!("Track {id}"),
artist: "Artist".into(),
source: ProviderId::YouTube,
providers: HashMap::new(),
};
t.set_provider(
ProviderId::YouTube,
ProviderTrack {
id: id.into(),
url: url.into(),
artist_id: None,
duration: 10,
thumbnail: String::new(),
album: None,
play_count: 0,
},
);
t
}
#[test]
fn play_queue_advance_and_restore_previous() {
let mut q = PlayQueue::new();
q.tracks = vec![
make_track("1", "url1"),
make_track("2", "url2"),
make_track("3", "url3"),
];
assert_eq!(
q.current().map(|t| t.provider_id(ProviderId::YouTube)),
Some(Some("1"))
);
assert!(q.advance());
assert_eq!(
q.current().map(|t| t.provider_id(ProviderId::YouTube)),
Some(Some("2"))
);
let t1 = make_track("1", "url1");
q.record_played(&t1, 50);
assert!(q.restore_previous());
assert_eq!(
q.current().map(|t| t.provider_id(ProviderId::YouTube)),
Some(Some("1"))
);
assert!(q.advance());
assert_eq!(
q.current().map(|t| t.provider_id(ProviderId::YouTube)),
Some(Some("2"))
);
assert!(q.advance());
assert_eq!(
q.current().map(|t| t.provider_id(ProviderId::YouTube)),
Some(Some("3"))
);
assert!(q.advance());
assert!(q.current().is_none());
assert!(!q.advance());
}
#[test]
fn play_queue_empty() {
let mut q = PlayQueue::new();
assert!(q.current().is_none());
assert!(!q.advance());
assert!(!q.restore_previous());
}
#[test]
fn record_played_order_and_dedup() {
let mut q = PlayQueue::new();
let t1 = make_track("1", "url1");
let t2 = make_track("2", "url2");
let t3 = make_track("3", "url3");
q.record_played(&t1, 50);
q.record_played(&t2, 50);
q.record_played(&t3, 50);
assert_eq!(q.recently_played.len(), 3);
assert_eq!(
q.recently_played[0].provider_id(ProviderId::YouTube),
Some("3")
);
assert_eq!(
q.recently_played[2].provider_id(ProviderId::YouTube),
Some("1")
);
q.record_played(&t2, 50);
assert_eq!(q.recently_played.len(), 3);
assert_eq!(
q.recently_played[0].provider_id(ProviderId::YouTube),
Some("2")
);
assert_eq!(
q.recently_played[1].provider_id(ProviderId::YouTube),
Some("3")
);
assert_eq!(
q.recently_played[2].provider_id(ProviderId::YouTube),
Some("1")
);
}
#[test]
fn record_played_truncates_to_max() {
let mut q = PlayQueue::new();
for i in 1..=60 {
q.record_played(&make_track(&i.to_string(), &format!("url{i}")), 50);
}
assert_eq!(q.recently_played.len(), 50);
assert_eq!(
q.recently_played[0].provider_id(ProviderId::YouTube),
Some("60")
);
assert_eq!(
q.recently_played[49].provider_id(ProviderId::YouTube),
Some("11")
);
}
#[test]
fn queue_tab_serde() {
let json = r#"{"tracks":[],"recently_played":[],"queue_tab":"RecentlyPlayed"}"#;
let q: PlayQueue = serde_json::from_str(json).unwrap();
assert_eq!(q.queue_tab, QueueTab::RecentlyPlayed);
let serialized = serde_json::to_string(&q).unwrap();
let restored: PlayQueue = serde_json::from_str(&serialized).unwrap();
assert_eq!(restored.queue_tab, QueueTab::RecentlyPlayed);
}
}