use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
use crate::types::TuiLogger;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SortBy {
TimeAdded,
Duration,
Random,
MostPlayed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlaylistItem {
pub name: String,
pub id: String, pub url: String, pub num_played: u32, pub duration: String, pub time_added: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Playlist {
pub name: String,
pub id: String, pub created_at: u64,
pub last_updated: u64,
pub sort_by: SortBy,
pub items: Vec<PlaylistItem>,
}
impl Playlist {
pub fn new(name: impl Into<String>) -> Self {
let now = now_epoch();
Self {
name: name.into(),
id: Uuid::new_v4().to_string(),
created_at: now,
last_updated: now,
sort_by: SortBy::TimeAdded,
items: Vec::new(),
}
}
pub fn touch(&mut self) {
self.last_updated = now_epoch();
}
pub fn add_item(&mut self, mut item: PlaylistItem) {
if item.id.is_empty() {
item.id = Uuid::new_v4().to_string();
}
item.time_added = now_epoch();
self.items.push(item);
self.touch();
}
pub fn remove_item(&mut self, item_id: &str) -> bool {
let len_before = self.items.len();
self.items.retain(|i| i.id != item_id);
let removed = self.items.len() != len_before;
if removed {
self.touch();
}
removed
}
pub fn increment_play(&mut self, item_id: &str) {
if let Some(it) = self.items.iter_mut().find(|i| i.id == item_id) {
it.num_played = it.num_played.saturating_add(1);
self.touch();
}
}
pub fn sort_items(&mut self) {
match self.sort_by {
SortBy::TimeAdded => self.items.sort_by_key(|i| i.time_added),
SortBy::Duration => self
.items
.sort_by_key(|i| parse_duration_to_secs(&i.duration).unwrap_or(0)),
SortBy::Random => {
use rand::seq::SliceRandom;
let mut rng = rand::thread_rng();
self.items.shuffle(&mut rng);
}
SortBy::MostPlayed => self
.items
.sort_by(|a, b| b.num_played.cmp(&a.num_played)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PlaylistsStore {
pub playlists: Vec<Playlist>,
pub last_selected_playlist_id: Option<String>,
}
#[derive(Clone)]
pub struct PlaylistsManager {
path: PathBuf,
pub store: PlaylistsStore,
#[allow(dead_code)]
logger: Option<TuiLogger>,
}
impl PlaylistsManager {
pub fn new() -> Result<Self> {
let mut path = dirs::home_dir().context("home_dir not found")?;
path.push(".bckplayer");
if !path.exists() {
fs::create_dir_all(&path).with_context(|| format!("create dir: {:?}", path))?;
}
path.push("playlists.config");
let store = if path.exists() {
let data = fs::read_to_string(&path).with_context(|| format!("read: {:?}", path))?;
serde_json::from_str(&data).with_context(|| "parse playlists.config")?
} else {
PlaylistsStore::default()
};
Ok(Self { path, store, logger: None })
}
pub fn new_or_default() -> Self {
match Self::new() {
Ok(m) => m,
Err(_) => {
let path = PathBuf::from("playlists.config");
Self { path, store: PlaylistsStore::default(), logger: None }
}
}
}
pub fn with_logger(mut self, logger: Option<TuiLogger>) -> Self {
self.logger = logger;
self
}
pub fn save(&self) -> Result<()> {
let data = serde_json::to_string_pretty(&self.store)?;
fs::write(&self.path, data).with_context(|| format!("write: {:?}", self.path))?;
if let Some(l) = &self.logger { l.debug("Playlists saved to disk"); }
Ok(())
}
pub fn reload(&mut self) -> Result<()> {
if self.path.exists() {
let data = fs::read_to_string(&self.path).with_context(|| format!("read: {:?}", self.path))?;
self.store = serde_json::from_str(&data).with_context(|| "parse playlists.config")?;
if let Some(l) = &self.logger { l.debug("Playlists reloaded from disk"); }
}
Ok(())
}
pub fn snapshot(&self) -> PlaylistsStore {
self.store.clone()
}
pub fn list(&self) -> &[Playlist] {
&self.store.playlists
}
pub fn list_mut(&mut self) -> &mut [Playlist] {
&mut self.store.playlists
}
pub fn get_mut(&mut self, id: &str) -> Option<&mut Playlist> {
self.store.playlists.iter_mut().find(|p| p.id == id)
}
pub fn add_playlist(&mut self, name: impl Into<String>) -> &Playlist {
let playlist = Playlist::new(name);
self.store.playlists.push(playlist);
let idx = self.store.playlists.len() - 1;
if let Some(l) = &self.logger { l.info(&format!("Playlist created: {}", self.store.playlists[idx].name)); }
&self.store.playlists[idx]
}
pub fn remove_playlist(&mut self, id: &str) -> bool {
let len_before = self.store.playlists.len();
self.store.playlists.retain(|p| p.id != id);
let removed = self.store.playlists.len() != len_before;
if removed && self.store.last_selected_playlist_id.as_deref() == Some(id) {
self.store.last_selected_playlist_id = None;
}
if removed { if let Some(l) = &self.logger { l.info(&format!("Playlist deleted: {}", id)); } }
removed
}
pub fn ensure_default(&mut self) {
if self.store.playlists.is_empty() {
let p = Playlist::new("My Playlist");
self.store.last_selected_playlist_id = Some(p.id.clone());
self.store.playlists.push(p);
if let Some(l) = &self.logger { l.info("Created default playlist: My Playlist"); }
}
}
pub fn find_item(&self, playlist_id: &str, item_id: &str) -> Option<(usize, usize)> {
self.store.playlists.iter().enumerate().find_map(|(pi, p)| {
if p.id == playlist_id {
p.items.iter().enumerate().find_map(|(ii, it)| if it.id == item_id { Some((pi, ii)) } else { None })
} else { None }
})
}
}
fn now_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn parse_duration_to_secs(s: &str) -> Option<u64> {
if let Ok(v) = s.parse::<u64>() {
return Some(v);
}
let parts: Vec<&str> = s.split(':').collect();
match parts.as_slice() {
[mm, ss] => {
let m: u64 = mm.parse().ok()?;
let s: u64 = ss.parse().ok()?;
Some(m * 60 + s)
}
[hh, mm, ss] => {
let h: u64 = hh.parse().ok()?;
let m: u64 = mm.parse().ok()?;
let s: u64 = ss.parse().ok()?;
Some(h * 3600 + m * 60 + s)
}
_ => None,
}
}