use std::{
collections::HashMap,
fs, io,
path::{Path, PathBuf},
};
use blake3::Hash;
use lunar_lib::formatter::{FormatError, FormatTable, format_str};
use thiserror::Error;
use crate::{
config::common::{LoudnormConfig, common_config},
database::{CompareAndSwapTransaction, DatabaseEntry, DatabaseError},
errors::{LibraryError, MetadataError},
library::{
album::Album,
artist::{Artist, ArtistGroup, add_from_artists},
metadata::{
ALBUM_ARTIST_KEY, ALBUM_KEY, ARTIST_KEY, DATE_KEY, DISC_NUM_KEY, GENRE_KEY,
MetadataKey, TITLE_KEY, TRACK_NUM_KEY,
},
track::{
Track, TrackId,
lyric_data::LyricData,
track_meta::{TrackAlbumInfo, TrackMeta},
},
},
media_container::MediaContainer,
utils::pair_extension,
};
impl Track {
#[must_use]
pub fn new(
hash: Hash,
src_container: MediaContainer,
metadata: TrackMeta,
relative_path: PathBuf,
) -> Self {
Self {
id: TrackId::new(hash),
src_container,
lib_container: None,
relative_library_path: relative_path,
metadata,
loudnorm_analysis: None,
applied_loudnorm: None,
version: Track::VERSION_NUMBER,
}
}
}
impl Track {
#[must_use]
pub fn id(&self) -> TrackId {
self.id
}
#[must_use]
pub fn loudnorm(&self) -> Option<&LoudnormConfig> {
self.applied_loudnorm.as_ref()
}
#[must_use]
pub fn src_container(&self) -> &MediaContainer {
&self.src_container
}
#[must_use]
pub fn lib_container(&self) -> Option<&MediaContainer> {
self.lib_container.as_ref()
}
pub fn album(&self) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
if let Some(album_id) = self.metadata.album {
Ok(Some({
let album = Album::db_get(album_id)?.expect("Dangling album reference");
let reference = album
.track_refs()
.iter()
.find(|t| t.id == self.id)
.expect("Track not found in album");
let track_num = reference.track_num;
let disc_num = reference.disc_num;
TrackAlbumInfo {
album,
track_num,
disc_num,
}
}))
} else {
Ok(None)
}
}
pub fn tx_album(
&self,
cas_tx: &mut CompareAndSwapTransaction,
) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
if let Some(album_id) = self.metadata.album {
Ok(Some({
let album = cas_tx.tx_get(album_id)?.expect("Dangling album reference");
let reference = album
.track_refs()
.iter()
.find(|t| t.id == self.id)
.expect("Dangling album>track reference");
let track_num = reference.track_num;
let disc_num = reference.disc_num;
TrackAlbumInfo {
album,
track_num,
disc_num,
}
}))
} else {
Ok(None)
}
}
}
impl Track {
pub fn metadata_key_values(&self) -> Result<HashMap<String, String>, DatabaseError> {
let mut map = HashMap::new();
if let Some(track_album_info) = self.album()? {
map.insert(ALBUM_KEY, track_album_info.album.name.clone());
let artists = track_album_info
.album
.artists()
.artists()?
.iter()
.map(Artist::name)
.collect::<Vec<_>>()
.join(";");
map.insert(ALBUM_ARTIST_KEY, artists);
if let Some(track_num) = track_album_info.track_num
&& let Some(track_total) = track_album_info.album.track_total
{
map.insert(TRACK_NUM_KEY, format!("{track_num}/{track_total}"));
}
if let Some(disc_num) = track_album_info.disc_num
&& let Some(disc_total) = track_album_info.album.disc_total
{
map.insert(DISC_NUM_KEY, format!("{disc_num}/{disc_total}"));
}
}
let artists = self
.metadata
.artists
.artists()?
.iter()
.map(Artist::name)
.collect::<Vec<_>>()
.join(";");
map.insert(ARTIST_KEY, artists);
if let Some(date) = self.metadata.date {
map.insert(DATE_KEY, date.to_string());
}
if let Some(genre) = &self.metadata.genre {
map.insert(GENRE_KEY, genre.to_owned());
}
if let Some(title) = &self.metadata.title {
map.insert(TITLE_KEY, title.to_owned());
}
if let Some(lyric_data) = &self.metadata.lyric_data {
let (k, v) = lyric_data.get_metadata_value();
map.insert(k, v);
}
let mut collected = self.metadata.other.clone();
collected.extend(map.into_iter().map(|(k, v)| (k.to_owned(), v)));
Ok(collected)
}
pub fn tx_apply_metadata_key(
&self,
key: MetadataKey,
cas_tx: &mut CompareAndSwapTransaction,
) -> Result<(), MetadataError> {
let mut track = cas_tx
.tx_get(self.id())?
.ok_or(DatabaseError::MissingEntry)?;
match key {
MetadataKey::Album(v) => {
cas_tx.tracks_set_album(v.as_ref().map(Album::id), std::iter::once(&track.id()))?;
}
MetadataKey::Artist(v) => track.metadata.artists = ArtistGroup::from_artists(&v),
MetadataKey::Date(v) => track.metadata.date = v,
MetadataKey::DiscNum(v) => {
if let Some(album_id) = track.metadata.album {
let mut album = cas_tx.tx_get(album_id)?.expect("Dangling album reference");
let track_reference = album
.tracks
.iter_mut()
.find(|t| t.id == track.id())
.expect("Dangling album>track reference");
track_reference.disc_num = v;
cas_tx.tx_upsert(album_id, Some(album))?;
} else {
return Err(MetadataError::MissingAlbum(
"disc cannot be set because track has no album".to_owned(),
));
}
}
MetadataKey::Genre(v) => track.metadata.genre = v,
MetadataKey::Lyrics(v) => track.metadata.lyric_data = v,
MetadataKey::Instrumental(v) => {
track.metadata.lyric_data = v.then_some(LyricData::Instrumental);
}
MetadataKey::Title(v) => track.metadata.title = v,
MetadataKey::TrackNum(v) => {
if let Some(album_id) = track.metadata.album {
let mut album = cas_tx.tx_get(album_id)?.expect("Dangling album reference");
let track_reference = album
.tracks
.iter_mut()
.find(|t| t.id == track.id())
.expect("Dangling album>track reference");
track_reference.track_num = v;
cas_tx.tx_upsert(album_id, Some(album))?;
} else {
return Err(MetadataError::MissingAlbum(
"track cannot be set because track has no album".to_owned(),
));
}
}
MetadataKey::Other(key, v) => {
if let Some(v) = v {
track.metadata.other.insert(key, v);
} else {
track.metadata.other.remove(&key);
}
}
_ => {
return Err(MetadataError::KeyNotAllowed(format!(
"{key} cannot be used on track metadata",
key = key.to_key()
)));
}
}
cas_tx.tx_upsert(track.id(), Some(track))?;
Ok(())
}
}
impl Track {
pub fn migrate(&mut self, library_dir: impl AsRef<Path>) -> Result<(), TrackRenameError> {
let Some(lib_container) = &mut self.lib_container else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"A library container was not found for the input track".to_string(),
)
.into());
};
let absolute_path = library_dir.as_ref().join(&self.relative_library_path);
fs::create_dir_all(absolute_path.parent().expect("File cannot be root"))?;
if let Err(err) = fs::rename(lib_container.path(), &absolute_path) {
match err.kind() {
io::ErrorKind::NotFound if fs::symlink_metadata(&absolute_path)?.is_file() => {}
_ => return Err(err.into()),
}
}
lib_container.set_path(absolute_path);
self.db_patch()?;
Ok(())
}
}
#[derive(Debug, Error)]
pub enum TrackRenameError {
#[error("IoError: {0}")]
Io(#[from] std::io::Error),
#[error("DatabaseError: {0}")]
Database(#[from] DatabaseError),
#[error("FormatError: {0}")]
Format(#[from] FormatError),
#[error("LibraryError: {0}")]
Library(#[from] LibraryError),
#[error("{0}")]
ConflictingNames(String),
}
pub fn calculate_rel_path(
metadata: &TrackMeta,
container_ref: &MediaContainer,
) -> Result<PathBuf, TrackRenameError> {
let artists = metadata.artists.artists()?;
let album = metadata.album()?;
let mut format_table = FormatTable::new();
format_table.extend_from_taggable(metadata);
add_from_artists(&mut format_table, &artists, "track");
if let Some(album) = album {
format_table.extend_from_taggable(&album);
}
let mut path = PathBuf::from(format_str(
&common_config().track_name_config.format_string,
&format_table,
)?);
path.add_extension(
pair_extension(container_ref.container(), container_ref.codec())
.expect("Invalid container/codec pair when renaming"),
);
Ok(path)
}