rave/rave/src/scan/flac.rs
Lyssieth 7b78375402
feat: improvements and bug fixes (sarcasm)
- `rest/getArtists` and `rest/getArtist`
- Improved artist formatting algorithm
- Other dispersed improvements
2023-10-18 23:05:09 +03:00

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),
}
}