325 lines
9.5 KiB
Rust
325 lines
9.5 KiB
Rust
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> {
|
|
Artist::find_all().await.unwrap()
|
|
}
|
|
|
|
pub async fn get_albums_by_artist(&self, artist: &str) -> Vec<Album> {
|
|
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> {
|
|
Track::find(
|
|
doc! {
|
|
"album_id": None::<String>,
|
|
"artist_id": reference_of!(Artist, artist).unwrap()
|
|
},
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn get_album_by_id(&self, album: &str) -> Option<Album> {
|
|
Album::get(album).await
|
|
}
|
|
|
|
pub async fn get_track_by_id(&self, track_id: &str) -> Option<Track> {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|