- `rest/getArtists` and `rest/getArtist` - Improved artist formatting algorithm - Other dispersed improvements
269 lines
8.2 KiB
Rust
269 lines
8.2 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::{borrow::Cow, result::Result};
|
|
|
|
use audiotags::{AudioTagEdit, FlacTag, MimeType, Tag};
|
|
use color_eyre::Report;
|
|
use entities::{
|
|
album, artist,
|
|
prelude::{Album, Artist, Track},
|
|
track,
|
|
};
|
|
use sea_orm::{
|
|
ActiveModelBehavior, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, Set,
|
|
};
|
|
use time::OffsetDateTime;
|
|
use tokio::{fs::File, sync::RwLock};
|
|
use tracing::{error, instrument, warn};
|
|
|
|
use super::ScanState;
|
|
|
|
#[instrument(skip(tx, state))]
|
|
pub async fn handle(
|
|
tx: &DatabaseTransaction,
|
|
path: PathBuf,
|
|
stem: Cow<'_, str>,
|
|
state: Arc<RwLock<ScanState>>,
|
|
) -> Result<track::Model, Report> {
|
|
let meta = { File::open(&path).await?.metadata().await? };
|
|
|
|
let tag = {
|
|
let tag = Tag::default()
|
|
.read_from_path(&path)
|
|
.map_err(|e| Report::msg("check ID3 tags for invalid encodings").wrap_err(e))?;
|
|
|
|
FlacTag::from(tag)
|
|
};
|
|
|
|
let album_artist = find_album_artist(tx, &tag).await?;
|
|
|
|
let album = find_album(tx, album_artist.as_ref().map(|c| c.id), &tag, state.clone()).await?;
|
|
|
|
if let Some(track) = Track::find()
|
|
.filter(track::Column::Path.eq(path.to_string_lossy()))
|
|
.one(tx)
|
|
.await
|
|
.map_err(|v| Report::msg("error searching for track").wrap_err(v))?
|
|
{
|
|
return Ok(track); // early exit if we already have this track. need to do an update check though :/
|
|
}
|
|
|
|
let mut am = track::ActiveModel::new();
|
|
|
|
let title = tag.title().unwrap_or(&stem).to_string();
|
|
let title = title.replace("\0 ", "").replace('\0', ""); // fuck NULLs.
|
|
|
|
let track_artist = find_track_artist(tx, &tag).await?;
|
|
|
|
am.title = Set(title);
|
|
am.album_id = Set(Some(album.id));
|
|
am.artist_id = Set(track_artist.as_ref().map(|c| c.id));
|
|
am.content_type = Set("audio/flac".to_string());
|
|
am.suffix = Set("flac".to_string());
|
|
am.path = Set(path.to_string_lossy().to_string());
|
|
am.is_dir = Set(false);
|
|
am.track_number = Set(tag
|
|
.track_number()
|
|
.map(|v| i32::try_from(v).expect("failed to convert track to i32")));
|
|
if let Some(disc_number) = tag
|
|
.disc_number()
|
|
.map(|v| i32::try_from(v).expect("failed to convert disc to i32"))
|
|
{
|
|
am.disc_number = Set(disc_number);
|
|
}
|
|
am.is_video = Set(false);
|
|
am.duration = {
|
|
let duration = tag.duration();
|
|
let duration = duration.unwrap_or_default();
|
|
let duration = duration.round().rem_euclid(2f64.powi(32)) as i64;
|
|
|
|
Set(duration)
|
|
};
|
|
let cover_art = tag.album_cover();
|
|
if let Some(cover_art) = cover_art {
|
|
let data = match cover_art.mime_type {
|
|
MimeType::Png | MimeType::Jpeg | MimeType::Gif => Some(cover_art.data),
|
|
_ => {
|
|
warn!(
|
|
"Unknown cover art mime type: {mime_type:?}",
|
|
mime_type = cover_art.mime_type
|
|
);
|
|
None
|
|
}
|
|
};
|
|
|
|
if let Some(data) = data {
|
|
let cover_art = super::find_or_create_cover_art(tx, data, cover_art.mime_type)
|
|
.await
|
|
.map_err(|v| Report::msg("error getting cover art").wrap_err(v))?;
|
|
am.cover_art_id = Set(Some(cover_art.id));
|
|
}
|
|
}
|
|
am.created = Set(OffsetDateTime::now_utc());
|
|
am.size = Set(meta
|
|
.len()
|
|
.try_into()
|
|
.expect("Failed to convert meta len to i64"));
|
|
|
|
let model = Track::insert(am)
|
|
.exec_with_returning(tx)
|
|
.await
|
|
.map_err(|v| Report::msg("couldn't add track").wrap_err(v))?;
|
|
|
|
Ok(model)
|
|
}
|
|
|
|
#[instrument(skip(tx, tag, state))]
|
|
async fn find_album(
|
|
tx: &DatabaseTransaction,
|
|
artist_id: Option<i64>,
|
|
tag: &FlacTag,
|
|
state: Arc<RwLock<ScanState>>,
|
|
) -> Result<album::Model, Report> {
|
|
let album = tag.album().unwrap_or(audiotags::types::Album {
|
|
title: "Unknown Album",
|
|
artist: None,
|
|
cover: None,
|
|
});
|
|
|
|
// if not, search by name
|
|
let search = Album::find()
|
|
.filter(album::Column::Name.like(album.title))
|
|
.one(tx)
|
|
.await?;
|
|
|
|
// if we found one, return it
|
|
if let Some(search) = search {
|
|
return Ok(search);
|
|
}
|
|
|
|
// otherwise, create it
|
|
let mut am = album::ActiveModel::new();
|
|
|
|
am.name = Set(album.title.to_string().replace("\0 ", "").replace('\0', ""));
|
|
am.music_folder_id = Set(state.read().await.music_folder_id);
|
|
am.artist_id = Set(artist_id);
|
|
am.year = Set(tag.year());
|
|
|
|
let genre = tag.genre();
|
|
if let Some(genre) = genre {
|
|
if genre.contains('\0') {
|
|
let genre_ids = genre
|
|
.split('\0')
|
|
.map(|genre| super::find_or_create_genre(tx, genre))
|
|
.collect::<Vec<_>>();
|
|
let genre_ids = futures::future::join_all(genre_ids).await;
|
|
|
|
let genre_ids = genre_ids
|
|
.into_iter()
|
|
.filter_map(Result::ok)
|
|
.collect::<Vec<_>>();
|
|
|
|
am.genre_ids = Set(Some(genre_ids));
|
|
} else {
|
|
let genre_id = super::find_or_create_genre(tx, genre).await?;
|
|
am.genre_ids = Set(Some(vec![genre_id]));
|
|
}
|
|
}
|
|
am.song_count = Set(tag
|
|
.total_tracks()
|
|
.map(|v| i64::try_from(v).expect("Failed to convert total tracks to i32"))
|
|
.unwrap_or_default());
|
|
am.duration = Set(tag
|
|
.duration()
|
|
.map(|c| c.round().rem_euclid(2f64.powi(32)) as i64) // TODO: figure out how to do this properly
|
|
.unwrap_or_default());
|
|
am.created = Set(OffsetDateTime::now_utc());
|
|
let cover_art = tag.album_cover();
|
|
if let Some(cover_art) = cover_art {
|
|
let data = match cover_art.mime_type {
|
|
MimeType::Png | MimeType::Jpeg | MimeType::Gif => Some(cover_art.data),
|
|
_ => {
|
|
warn!(
|
|
"Unknown cover art mime type: {mime_type:?}",
|
|
mime_type = cover_art.mime_type
|
|
);
|
|
None
|
|
}
|
|
};
|
|
|
|
if let Some(data) = data {
|
|
let cover_art = super::find_or_create_cover_art(tx, data, cover_art.mime_type).await?;
|
|
am.cover_art_id = Set(Some(cover_art.id));
|
|
}
|
|
}
|
|
|
|
let model = Album::insert(am).exec_with_returning(tx).await;
|
|
|
|
// if we failed to insert, return the error
|
|
let Ok(model) = model else {
|
|
let err = model.expect_err("somehow not err");
|
|
error!(
|
|
error = &err as &dyn std::error::Error,
|
|
"Failed to insert album {err}"
|
|
);
|
|
|
|
return Err(Report::new(err));
|
|
};
|
|
|
|
Ok(model)
|
|
}
|
|
|
|
#[instrument(skip(tx, tag))]
|
|
async fn find_album_artist(
|
|
tx: &DatabaseTransaction,
|
|
tag: &FlacTag,
|
|
) -> Result<Option<artist::Model>, Report> {
|
|
let artist_to_search = crate::utils::format_flac_artist_for_album(tag);
|
|
|
|
find_artist(tx, artist_to_search).await
|
|
}
|
|
|
|
#[instrument(skip(tx, tag))]
|
|
async fn find_track_artist(
|
|
tx: &DatabaseTransaction,
|
|
tag: &FlacTag,
|
|
) -> Result<Option<artist::Model>, Report> {
|
|
let artist_to_search = crate::utils::format_flac_artist_for_track(tag);
|
|
|
|
find_artist(tx, artist_to_search).await
|
|
}
|
|
|
|
#[instrument(skip(tx))]
|
|
async fn find_artist(
|
|
tx: &DatabaseTransaction,
|
|
artist_to_search: Option<String>,
|
|
) -> Result<Option<artist::Model>, Report> {
|
|
match &artist_to_search {
|
|
Some(artist_to_search) => {
|
|
let artist_to_search = artist_to_search.trim();
|
|
let attempt = Artist::find()
|
|
.filter(artist::Column::Name.eq(artist_to_search))
|
|
.one(tx)
|
|
.await?;
|
|
|
|
if let Some(attempt) = attempt {
|
|
Ok(Some(attempt))
|
|
} else {
|
|
let am = artist::ActiveModel {
|
|
name: Set(artist_to_search.to_string()),
|
|
..Default::default()
|
|
};
|
|
let model = Artist::insert(am).exec_with_returning(tx).await;
|
|
|
|
let Ok(model) = model else {
|
|
let err = model.expect_err("somehow not err");
|
|
error!(
|
|
error = &err as &dyn std::error::Error,
|
|
"Failed to insert artist {err}"
|
|
);
|
|
|
|
return Err(Report::new(err));
|
|
};
|
|
|
|
Ok(Some(model))
|
|
}
|
|
}
|
|
None => Ok(None),
|
|
}
|
|
}
|