use std::{
collections::HashMap,
fs,
io::{self},
path::Path,
sync::Arc,
};
use barber::{ProgressBar, ProgressRenderer};
use image::ImageError;
use lunar_lib::{
database::{DatabaseEntry, DbHandle, TransactionError, db_transaction},
iterator_ext::IteratorExtensions,
paths::sys::sanitize_str,
};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error;
use crate::{
data_dir,
database::LibraryDb,
library::{
album::{Album, AlbumId},
image_art::{CacheableArt, ImageArt},
track::{Track, TrackId},
},
lyrics::synced_lyrics::LyricParseError,
};
use super::track::lyric_data::LyricData;
#[derive(Debug, Error)]
pub enum LinkingError {
#[error("IoError: {0}")]
Io(#[from] std::io::Error),
#[error("ImageError: {0}")]
Image(#[from] ImageError),
#[error("Transaction Error: {0}")]
Transaction(#[from] TransactionError),
#[error("LyricParse Error: {0}")]
LyricParse(#[from] LyricParseError),
}
pub fn smart_link(
progress_renderer: Arc<dyn ProgressRenderer>,
db: DbHandle<LibraryDb>,
) -> Result<(), LinkingError> {
let mut all_albums: HashMap<AlbumId, Album> = Album::db_get_all(&db)?
.into_iter()
.map(|a| (a.id(), a))
.collect();
let mut all_tracks: HashMap<TrackId, Track> = Track::db_get_all(&db)?
.into_iter()
.map(|t| (t.id(), t))
.collect();
let no_art = all_albums
.iter_mut()
.filter_map(|(_, a)| {
if a.art.as_ref().is_none_or(|a| !a.source().exists()) {
Some(a)
} else {
None
}
})
.to_vec();
find_album_art(no_art, &all_tracks, progress_renderer.clone())?;
find_lyrics(all_tracks.values_mut(), progress_renderer)?;
db_transaction(
|cas_tx| {
for track in all_tracks.values() {
cas_tx.tx_upsert(track.clone())?;
}
for album in all_albums.values() {
cas_tx.tx_upsert(album.clone())?;
}
Ok(())
},
db,
false,
)
.map_err(TransactionError::from)?;
Ok(())
}
fn find_lyrics<'a>(
tracks: impl IntoIterator<Item = &'a mut Track> + ExactSizeIterator,
progress_renderer: Arc<dyn ProgressRenderer + 'static>,
) -> Result<(), LinkingError> {
let has_lyric_file = tracks
.into_iter()
.filter_map(|track| {
let mut lrc_path = track.container().path().to_path_buf();
lrc_path.set_extension("slrc");
if lrc_path.exists() {
return Some((track, lrc_path));
}
lrc_path.set_extension("lrc");
if lrc_path.exists() {
return Some((track, lrc_path));
}
None
})
.to_vec();
let progress_bar = ProgressBar::new(0, has_lyric_file.len(), progress_renderer);
for (track, lrc_file) in has_lyric_file {
let string = fs::read_to_string(lrc_file)?;
track.metadata.lyric_data = Some(LyricData::infer_from_string(string)?);
progress_bar.set_label(&format!(
"Linked lyric files for {}",
track.metadata.safe_title()
));
progress_bar.increment();
}
Ok(())
}
fn find_album_art(
albums: Vec<&mut Album>,
tracks: &HashMap<TrackId, Track>,
progress_renderer: Arc<dyn ProgressRenderer>,
) -> Result<(), ImageError> {
let progress_bar = ProgressBar::new(0, albums.len(), progress_renderer);
albums
.into_par_iter()
.try_for_each(|album| -> Result<(), ImageError> {
if album.tracks.is_empty() {
return Ok(());
}
let album_tracks: Vec<&Track> = album
.tracks
.iter()
.map(|tr| tracks.get(&tr.id).expect("Dangling pointer"))
.collect();
let parent_dir = album_tracks[0]
.container()
.path()
.parent()
.expect("Files must have parents");
let all_same_parent = album_tracks
.iter()
.all(|t| t.container().path().parent() == Some(parent_dir));
if all_same_parent
&& let Some(image_art) = art_from_dir(parent_dir, album.tracks.len())?
{
album.art = Some(image_art);
progress_bar.set_label(&format!("Found cover art file for {}", album.name));
progress_bar.increment();
return Ok(());
}
if let Some(cover_art) = &album_tracks[0].metadata().art
&& album_tracks.iter().all(|t| {
t.metadata()
.art
.as_ref()
.is_some_and(|a| a.hash() == cover_art.hash())
})
{
let export_path =
data_dir().join(format!("album_art/{}", sanitize_str(&album.name)));
album.art = Some(cover_art.export_to_image_art(&export_path)?);
progress_bar.set_label(&format!("Assumed cover art for {}", album.name));
progress_bar.increment();
return Ok(());
}
progress_bar.set_label(&format!("Couldn't find cover art for {}", album.name));
progress_bar.increment();
Ok(())
})?;
progress_bar.flush();
Ok(())
}
fn art_from_dir(dir: impl AsRef<Path>, expected_file_count: usize) -> io::Result<Option<ImageArt>> {
const COVER_LOOKUP_EXT: [&str; 5] = ["png", "jpeg", "jpg", "bmp", "tiff"];
let read_dir = fs::read_dir(dir.as_ref())?.flatten().to_vec();
if read_dir.len() != expected_file_count && read_dir.len() != expected_file_count + 1 {
return Ok(None);
}
let mut image = None;
for entry in read_dir {
if !entry.file_type().is_ok_and(|t| t.is_file()) {
continue;
}
let path = entry.path();
if path
.extension()
.and_then(std::ffi::OsStr::to_str)
.is_some_and(|ext| COVER_LOOKUP_EXT.contains(&ext))
&& let Ok(image_art) = ImageArt::from_file(path)
{
image = Some(image_art);
}
}
Ok(image)
}