selene-core 0.8.1

selene-core is the backend for Selene, a local-first music player
Documentation
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(())
}