use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::{
fs,
path::{Path, PathBuf},
};
pub type TrackId = u64;
pub type PlaylistId = u64;
pub const ALL_TRACKS_ID: PlaylistId = 0;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Track {
pub id: TrackId,
pub name: String,
pub path: PathBuf,
}
impl Track {
pub fn new(id: TrackId, path: impl Into<PathBuf>) -> Self {
let path: PathBuf = path.into();
let name = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "Unknown".to_string());
Self { id, name, path }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Playlist {
pub id: PlaylistId,
pub name: String,
pub tracks: Vec<TrackId>,
}
impl Playlist {
pub fn new(id: PlaylistId, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
tracks: Vec::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Library {
pub tracks: Vec<Track>,
pub playlists: Vec<Playlist>,
next_track_id: TrackId,
next_playlist_id: PlaylistId,
}
impl Default for Library {
fn default() -> Self {
Self {
tracks: Vec::new(),
playlists: vec![Playlist::new(ALL_TRACKS_ID, "All Tracks")],
next_track_id: 1,
next_playlist_id: 1, }
}
}
impl Library {
pub fn data_dir() -> Result<PathBuf> {
let home = dirs_next::home_dir().context("could not determine home directory")?;
Ok(home.join(".audium"))
}
pub fn music_dir() -> Result<PathBuf> {
Ok(Self::data_dir()?.join("music"))
}
fn index_path() -> Result<PathBuf> {
Ok(Self::data_dir()?.join("library.json"))
}
pub fn load() -> Result<Self> {
fs::create_dir_all(Self::data_dir()?)?;
fs::create_dir_all(Self::music_dir()?)?;
let index = Self::index_path()?;
if !index.exists() {
return Ok(Self::default());
}
let raw = fs::read_to_string(&index)
.with_context(|| format!("reading library index at {}", index.display()))?;
let mut lib: Self = serde_json::from_str(&raw)
.with_context(|| "parsing library.json — the file may be corrupted")?;
let before: std::collections::HashSet<TrackId> = lib.tracks.iter().map(|t| t.id).collect();
lib.tracks.retain(|t| t.path.exists());
let after: std::collections::HashSet<TrackId> = lib.tracks.iter().map(|t| t.id).collect();
let removed: std::collections::HashSet<TrackId> =
before.difference(&after).copied().collect();
if !removed.is_empty() {
for pl in &mut lib.playlists {
pl.tracks.retain(|id| !removed.contains(id));
}
}
if lib.playlists.is_empty() || lib.playlists[0].id != ALL_TRACKS_ID {
lib.playlists.retain(|pl| pl.id != ALL_TRACKS_ID);
let mut all = Playlist::new(ALL_TRACKS_ID, "All Tracks");
all.tracks = lib.tracks.iter().map(|t| t.id).collect();
lib.playlists.insert(0, all);
}
Ok(lib)
}
pub fn save(&self) -> Result<()> {
let index = Self::index_path()?;
let raw = serde_json::to_string_pretty(self)?;
fs::write(&index, raw)
.with_context(|| format!("writing library index to {}", index.display()))?;
Ok(())
}
pub fn add_file(&mut self, source: &Path) -> Result<(Track, bool)> {
let music_dir = Self::music_dir()?;
let filename = source.file_name().context("source path has no filename")?;
let dest = music_dir.join(filename);
if !dest.exists() {
fs::copy(source, &dest)
.with_context(|| format!("copying {} -> {}", source.display(), dest.display()))?;
}
let dest_canon = dest.canonicalize().ok();
if let Some(existing) = self
.tracks
.iter()
.find(|t| t.path.canonicalize().ok() == dest_canon)
{
return Ok((existing.clone(), false));
}
let id = self.next_track_id;
self.next_track_id += 1;
let track = Track::new(id, &dest);
self.tracks.push(track.clone());
if let Some(all) = self.playlists.iter_mut().find(|p| p.id == ALL_TRACKS_ID) {
all.tracks.push(id);
}
self.save()?;
Ok((track, true))
}
pub fn remove_track(&mut self, id: TrackId) -> Result<()> {
self.tracks.retain(|t| t.id != id);
for pl in &mut self.playlists {
pl.tracks.retain(|&tid| tid != id);
}
self.save()
}
pub fn rename_track(&mut self, id: TrackId, new_name: impl Into<String>) -> Result<()> {
if let Some(t) = self.tracks.iter_mut().find(|t| t.id == id) {
t.name = new_name.into();
}
self.save()
}
pub fn track(&self, id: TrackId) -> Option<&Track> {
self.tracks.iter().find(|t| t.id == id)
}
pub fn create_playlist(&mut self, name: impl Into<String>) -> Result<PlaylistId> {
let id = self.next_playlist_id;
self.next_playlist_id += 1;
self.playlists.push(Playlist::new(id, name));
self.save()?;
Ok(id)
}
pub fn delete_playlist(&mut self, id: PlaylistId) -> Result<()> {
if id == ALL_TRACKS_ID {
return Ok(());
}
self.playlists.retain(|p| p.id != id);
self.save()
}
pub fn rename_playlist(&mut self, id: PlaylistId, name: impl Into<String>) -> Result<()> {
if id == ALL_TRACKS_ID {
return Ok(());
}
if let Some(pl) = self.playlists.iter_mut().find(|p| p.id == id) {
pl.name = name.into();
}
self.save()
}
pub fn playlist_add_track(&mut self, playlist_id: PlaylistId, track_id: TrackId) -> Result<()> {
if let Some(pl) = self.playlists.iter_mut().find(|p| p.id == playlist_id)
&& !pl.tracks.contains(&track_id)
{
pl.tracks.push(track_id);
}
self.save()
}
pub fn playlist_remove_track(
&mut self,
playlist_id: PlaylistId,
track_id: TrackId,
) -> Result<()> {
if let Some(pl) = self.playlists.iter_mut().find(|p| p.id == playlist_id) {
pl.tracks.retain(|&id| id != track_id);
}
self.save()
}
pub fn playlist(&self, id: PlaylistId) -> Option<&Playlist> {
self.playlists.iter().find(|p| p.id == id)
}
pub fn playlist_tracks(&self, playlist_id: PlaylistId) -> Vec<&Track> {
self.playlist(playlist_id)
.map(|pl| pl.tracks.iter().filter_map(|id| self.track(*id)).collect())
.unwrap_or_default()
}
}