use std::{
fs, io,
path::{Path, PathBuf},
};
use blake3::Hash;
use chrono::{Datelike, Timelike};
use lofty::{
ogg::VorbisComments,
tag::{Accessor, Tag, TagType, items::Timestamp},
};
use lunar_lib::{
database::{
CompareAndSwapTransaction, Database, DatabaseEntry, DatabaseError, TransactionError,
},
formatter::{FormatError, FormatTable, format_str},
};
use thiserror::Error;
use crate::{
config::common::{LoudnormSettings, common_config},
database::{LibraryDb, entry_extensions::EntryExtensions},
errors::{LibraryError, MetadataError},
library::{
album::Album,
artist::{Artist, add_from_artists},
track::{
Track, TrackId,
track_meta::{TrackAlbumInfo, TrackMeta},
},
},
media_container::{ContainerFormat, MediaContainer},
};
use super::lyric_data::LyricData;
impl Track {
#[must_use]
pub fn new(
hash: Hash,
src_container: MediaContainer,
metadata: TrackMeta,
relative_library_path: PathBuf,
) -> Self {
Self {
id: TrackId::new(hash),
src_container,
lib_container: None,
relative_library_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<&LoudnormSettings> {
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, db: &LibraryDb) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
if let Some(album_id) = self.metadata.album {
Ok(Some({
let album = Album::db_get_from(album_id, db)?.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<LibraryDb>,
) -> Result<Option<TrackAlbumInfo>, TransactionError> {
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,
format: &ContainerFormat,
) -> Result<Vec<(String, String)>, MetadataError> {
let mut tags = VorbisComments::new();
{
let db = LibraryDb::open()?;
if let Some(track_album_info) = self.album(&db)? {
tags.set_album(track_album_info.album.name.clone());
let artists = track_album_info
.album
.artists()
.artists(&db)?
.iter()
.map(Artist::name)
.collect::<Vec<_>>()
.join(";");
tags.insert("ALBUMARTIST".to_owned(), artists);
if let Some(track_num) = track_album_info.track_num {
tags.set_track(track_num);
if let Some(track_total) = track_album_info.album.track_total {
tags.set_track_total(track_total);
}
}
if let Some(disc_num) = track_album_info.disc_num {
tags.set_disk(disc_num);
if let Some(disc_total) = track_album_info.album.disc_total {
tags.set_disk_total(disc_total);
}
}
}
let artists = self
.metadata
.artists
.artists(&db)?
.iter()
.map(Artist::name)
.collect::<Vec<_>>()
.join(";");
tags.set_artist(artists);
}
if let Some(date) = self.metadata.date {
let ts = Timestamp {
year: date.year() as u16,
month: Some(date.month() as u8),
day: Some(date.day() as u8),
hour: Some(date.hour() as u8),
minute: Some(date.minute() as u8),
second: Some(date.second() as u8),
};
tags.set_date(ts);
}
for genre in &self.metadata.genre {
tags.push("GENRE".to_owned(), genre.to_owned());
}
if let Some(title) = &self.metadata.title {
tags.set_title(title.to_owned());
}
if let Some(lyric_data) = &self.metadata.lyric_data {
match lyric_data {
LyricData::Instrumental => {
tags.insert("INSTRUMENTAL".to_owned(), "1".to_owned());
}
LyricData::Plain(lyrics) => {
tags.insert("UNSYNCEDLYRICS".to_owned(), lyrics.to_string());
}
LyricData::Synced(lyrics) => {
tags.insert("SYNCEDLYRICS".to_owned(), lyrics.to_lrc_string());
}
}
}
self.metadata.other.iter().for_each(|(k, v)| {
tags.insert(k.to_uppercase(), v.to_owned());
});
let _ = tags.remove("ENCODEDBY");
let tag_type = match format {
ContainerFormat::Flac | ContainerFormat::Ogg => TagType::VorbisComments,
ContainerFormat::Mpa | ContainerFormat::Wav => TagType::Id3v2,
ContainerFormat::Aiff => TagType::AiffText,
ContainerFormat::Ape => TagType::Ape,
};
let mut tags: Tag = tags.into();
tags.re_map(tag_type);
let tags = tags
.items()
.map(|tag| {
(
tag.key().map_key(tag_type).unwrap().to_owned(),
tag.value().text().unwrap().to_owned(),
)
})
.collect();
Ok(tags)
}
}
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);
Track::db_patch(self.clone(), None)?;
Ok(())
}
}
#[derive(Debug, Error)]
pub enum TrackRenameError {
#[error("IoError: {0}")]
Io(#[from] std::io::Error),
#[error("DatabaseError: {0}")]
Database(#[from] DatabaseError),
#[error("TransactionError: {0}")]
Transaction(#[from] TransactionError),
#[error("FormatError: {0}")]
Format(#[from] FormatError),
#[error("LibraryError: {0}")]
Library(#[from] LibraryError),
#[error("{0}")]
ConflictingNames(String),
}
pub fn calculate_rel_path(metadata: &TrackMeta) -> Result<PathBuf, TrackRenameError> {
let db = LibraryDb::open()?;
let artists = metadata.artists.artists(&db)?;
let album = metadata.album(&db)?;
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 path = PathBuf::from(format_str(
&common_config().track_name.format_string,
&format_table,
)?);
Ok(path)
}