use std::{collections::HashMap, sync::Arc};
use barber::ProgressRenderer;
use image::ImageError;
use lunar_lib::{
database::{DatabaseEntry, DbHandle, TransactionError, db_transaction},
iterator_ext::IteratorExtensions,
paths::sys::sanitize_str,
};
use thiserror::Error;
use crate::{
data_dir,
database::LibraryDb,
library::{
album::{Album, AlbumId},
image_art::CacheableArt,
linking::{album_track_linking::album_track_linking, lyric_linking::link_lrc_files},
track::{Track, TrackId},
},
lyrics::synced_lyrics::LyricParseError,
};
mod album_track_linking;
mod lyric_linking;
#[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_tracks: HashMap<TrackId, (Track, bool)> = Track::db_get_all(&db)?
.into_iter()
.map(|t| (t.id(), (t, false)))
.collect();
let mut all_albums: HashMap<AlbumId, (Album, bool)> = Album::db_get_all(&db)?
.into_iter()
.map(|t| (t.id(), (t, false)))
.collect();
link_lrc_files(all_tracks.values_mut(), progress_renderer.clone())?;
{
let mut track_groups: HashMap<_, Vec<_>> = HashMap::new();
for (track, changed) in all_tracks.values_mut() {
let parent = track.container().path().parent().unwrap().to_owned();
track_groups
.entry(parent)
.or_default()
.push((track, changed));
}
album_track_linking(progress_renderer, &mut all_albums, &mut track_groups)?;
}
for (album, changed) in all_albums.values_mut() {
if album.art.is_some() {
continue;
}
let first_art = album
.tracks
.iter()
.find_map(|tr| all_tracks.get(&tr.id)?.0.metadata().art.as_ref());
let Some(cover_art) = first_art else { continue };
let hash = cover_art.hash();
let all_same = album
.tracks
.iter()
.filter_map(|tr| all_tracks.get(&tr.id))
.all(|(t, _)| t.metadata().art.as_ref().is_some_and(|a| a.hash() == hash));
if all_same {
let export_path = data_dir().join(format!("album_art/{}", sanitize_str(&album.name())));
album.art = Some(cover_art.export_to_image_art(&export_path)?);
*changed = true;
}
}
let changed_tracks = all_tracks
.into_iter()
.filter_map(|(_, (track, changed))| changed.then_some(track))
.to_vec();
let changed_albums = all_albums
.into_iter()
.filter_map(|(_, (album, changed))| changed.then_some(album))
.to_vec();
db_transaction(
|cas_tx| {
for track in &changed_tracks {
cas_tx.tx_upsert(track.clone())?;
}
for album in &changed_albums {
cas_tx.tx_upsert(album.clone())?;
}
Ok(())
},
db,
false,
)
.map_err(TransactionError::from)?;
Ok(())
}