use std::path::{Path, PathBuf}; use album::Album; use artist::Artist; use mongod::{reference_of, Model, Referencable, Reference}; use mongodb::bson::doc; use serde_json::json; use track::Track; use walkdir::WalkDir; use crate::cache::RouteCache; pub mod album; pub mod artist; pub mod metadata; pub mod playlist; pub mod track; pub mod user; /// Checks if a file has a music file extension fn is_music_file(path: &Path) -> bool { if let Some(extension) = path.extension() { match extension.to_str().unwrap_or("").to_lowercase().as_str() { "mp3" | "flac" | "wav" | "aac" | "ogg" | "m4a" | "opus" => return true, _ => return false, } } false } pub struct Libary { root_dir: PathBuf, } impl Libary { pub const fn new(root_dir: PathBuf) -> Self { Self { root_dir } } pub async fn find_or_create_artist(&self, artist: &str) -> Reference { if let Some(artist) = Artist::find_one(doc! { "name": artist }).await { artist.reference() } else { Artist::create(artist).await.reference() } } pub async fn find_or_create_album(&self, album: &str, artist_id: Option<&str>) -> Reference { if let Some(album) = Album::find_one(doc! { "title": album, "artist_id": artist_id}).await { album.reference() } else { Album::create(album, artist_id).await.reference() } } pub async fn add_path_to_library(&self, path: &str) { // search for path already present if Track::find_one_partial(doc! { "path": path }, json!({})) .await .is_some() { return; } log::info!("Adding {path} to library"); // add track to library let metadata = metadata::get_metadata(path); let mut entry = json!({ "_id": uuid::Uuid::new_v4().to_string(), "path": path, }); if let Some(meta) = &metadata { if let Some(artist) = meta.artist() { let artist_id = self.find_or_create_artist(artist).await; entry .as_object_mut() .unwrap() .insert("artist_id".into(), artist_id.into()); } else { log::warn!("{path} has no artist"); } if let Some(album) = meta.album() { let album_id = self .find_or_create_album( album, entry .as_object() .unwrap() .get("artist_id") .map(|x| x.as_str().unwrap()), ) .await; entry .as_object_mut() .unwrap() .insert("album_id".into(), album_id.into()); } else { log::warn!("{path} has no album and will be treated as single"); } if let Some(title) = meta.title() { entry .as_object_mut() .unwrap() .insert("title".into(), title.into()); } } if let Some(metadata) = metadata { entry .as_object_mut() .unwrap() .insert("meta".into(), metadata.0); } // if no title in metadata use file name if entry.as_object().unwrap().get("title").is_none() { entry.as_object_mut().unwrap().insert( "title".into(), std::path::Path::new(path) .file_stem() .unwrap() .to_str() .unwrap() .into(), ); } Track::create(entry.as_object().unwrap()).await; } pub async fn get_artists(&self) -> Vec { Artist::find_all().await.unwrap() } pub async fn get_albums_by_artist(&self, artist: &str) -> Vec { Album::find( doc! { "artist_id": reference_of!(Artist, artist).unwrap()}, None, None, ) .await .unwrap() } pub async fn get_singles_by_artist(&self, artist: &str) -> Vec { Track::find( doc! { "album_id": None::, "artist_id": reference_of!(Artist, artist).unwrap() }, None, None, ) .await .unwrap() } pub async fn get_album_by_id(&self, album: &str) -> Option { Album::get(album).await } pub async fn get_track_by_id(&self, track_id: &str) -> Option { Track::get(track_id).await } pub async fn reload_metadata(&self, track_id: &str) -> Result<(), ()> { let mut track = Track::get(track_id).await.ok_or_else(|| ())?; let path = &track.path; log::info!("Rescanning metadata for {path}"); let metadata = metadata::get_metadata(path); let mut update = json!({}); if let Some(meta) = &metadata { if let Some(artist) = meta.artist() { let artist_id = self.find_or_create_artist(artist).await; update .as_object_mut() .unwrap() .insert("artist_id".into(), artist_id.into()); } else { log::warn!("{path} has no artist"); } if let Some(album) = meta.album() { let album_id = self .find_or_create_album( album, update .as_object() .unwrap() .get("artist_id") .map(|x| x.as_str().unwrap()), ) .await; update .as_object_mut() .unwrap() .insert("album_id".into(), album_id.into()); } else { log::warn!("{path} has no album and will be treated as single"); } if let Some(title) = meta.title() { update .as_object_mut() .unwrap() .insert("title".into(), title.into()); } } if let Some(metadata) = metadata { update .as_object_mut() .unwrap() .insert("meta".into(), metadata.0); } // if no title in metadata use file name if update.as_object().unwrap().get("title").is_none() { update.as_object_mut().unwrap().insert( "title".into(), std::path::Path::new(&path) .file_stem() .unwrap() .to_str() .unwrap() .into(), ); } track.update(&update).await.unwrap(); Ok(()) } pub async fn clean_lost_files(&self) { // tracks for track in Track::find_partial(doc! {}, json!({"path": 1}), None, None) .await .unwrap() { if !std::path::Path::new(&track.path.as_ref().unwrap()).exists() { log::info!("Cleaning lost {}", track.path.as_ref().unwrap()); Track::remove(&track._id).await.unwrap(); } } // albums for album in Album::find_partial(doc! {}, json!({"title": 1}), None, None) .await .unwrap() { if Track::find_partial( doc! { "album_id": album.reference() }, json!({}), None, None, ) .await .unwrap() .is_empty() { log::info!( "Cleaning album {} with no tracks", album.title.as_ref().unwrap() ); Album::remove(album.id()).await.unwrap(); } } // artists for artist in Artist::find_partial(doc! {}, json!({"name": 1}), None, None) .await .unwrap() { if Track::find_partial( doc! { "artist_id": artist.reference()}, json!({}), None, None, ) .await .unwrap() .is_empty() && Album::find_partial( doc! { "artist_id": artist.reference()}, json!({}), None, None, ) .await .unwrap() .is_empty() { log::info!( "Cleaning artist {} with no tracks or albums", artist.name.as_ref().unwrap() ); Artist::remove(artist.id()).await.unwrap(); } } } pub async fn rescan(&self, cache: &RouteCache) { cache.invalidate("albums", "latest").await; log::info!("Rescanning library"); for entry in WalkDir::new(self.root_dir.clone()) .follow_links(true) .into_iter() .filter_map(std::result::Result::ok) { let path = entry.path(); if path.is_file() && is_music_file(path) { let path = path.to_string_lossy().to_string(); self.add_path_to_library(&path).await; } } } }