selene-core 0.4.2

selene-core is the backend for Selene, a local-first music player
Documentation
use std::sync::Arc;

use barber::{ProgressBar, ProgressRenderer};
use lunar_lib::database::{Database, DatabaseEntry, DatabaseError, TransactionError};
use thiserror::Error;

use crate::{
    database::{LibraryDb, entry_extensions::EntryExtensions},
    library::track::{Track, lyric_data::LyricData},
    lrclib::SearchQuery,
};

#[derive(Debug, Error)]
pub enum LyricSearchError {
    #[error("DatabaseError: {0}")]
    Database(#[from] DatabaseError),

    #[error("Transaction Error: {0}")]
    Transaction(#[from] TransactionError),

    #[error("ReqwestError: {0}")]
    Reqwest(#[from] reqwest::Error),
}

/// Scans the input `tracks` for tracks that need lyrics
///
/// Tracks need to be reimported if:
/// - They do not have known lyric information
/// - They have plain lyrics and `synced_only` is enabled
pub fn find_needs_lyrics(synced_only: bool) -> Result<Vec<Track>, DatabaseError> {
    let mut tracks = Track::db_get_all()?;

    tracks.retain(|track| {
        track
            .metadata
            .lyric_data
            .as_ref()
            .is_none_or(|l| matches!(l, LyricData::Plain(_)) && synced_only)
    });

    Ok(tracks)
}

pub fn find_lyrics_for(
    tracks: Vec<Track>,
    progress_renderer: Arc<dyn ProgressRenderer>,
    synced_only: bool,
) -> Result<(), LyricSearchError> {
    let progress_bar = ProgressBar::new(0, tracks.len(), progress_renderer);

    'track_loop: for mut track in tracks {
        let Some(track_title) = track.metadata.title.as_deref() else {
            progress_bar.set_label("Ignored track with unknown title");
            continue;
        };

        progress_bar.set_label(&format!("Searching lyrics for {track_title}..."));
        progress_bar.increment();

        let (album, artists) = {
            let db = LibraryDb::open().unwrap();
            (track.album(&db)?, track.metadata.artists(&db)?)
        };

        let album_title = album.map(|a| a.album.name);

        let results = SearchQuery::new(track_title)
            .album_title(album_title.as_deref())
            .main_artist(Some(artists[0].name()))
            .run()?;

        if results.is_empty() {
            continue;
        }

        for result in &results {
            if let Some(synced_lyrics) = result.synced_lyrics() {
                track.metadata.lyric_data = Some(LyricData::Synced(synced_lyrics));
                track.db_patch(None)?;
                continue 'track_loop;
            }
        }

        for result in &results {
            if let Some(synced_lyrics) = result.plain_lyrics() {
                if !synced_only {
                    track.metadata.lyric_data = Some(LyricData::Plain(synced_lyrics));
                }
                track.db_patch(None)?;
                continue 'track_loop;
            }
        }

        track.metadata.lyric_data = Some(LyricData::Instrumental);
        track.db_patch(None)?;
    }

    Ok(())
}