use std::sync::Arc;
use barber::{ProgressBar, ProgressRenderer};
use futures::{StreamExt, stream::FuturesUnordered};
use lunar_lib::database::{
Database, DatabaseEntry, DatabaseError, TransactionError, writer::DatabaseWriter,
};
use thiserror::Error;
use tokio::sync::Semaphore;
use crate::{
database::{LibraryDb, tx_extensions::CasTxExtensions},
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),
}
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 async fn find_lyrics_for(
tracks: Vec<Track>,
progress_renderer: Arc<dyn ProgressRenderer>,
synced_only: bool,
) -> Result<(), LyricSearchError> {
struct QueryItem {
track: Track,
query: SearchQuery,
}
let db = LibraryDb::open()?;
let mut prepared = Vec::with_capacity(tracks.len());
for track in tracks {
let Some(title) = track.metadata.title.as_deref() else {
continue;
};
let query = {
let album = track.album(&db)?;
let artists = track.metadata.artists(&db)?;
SearchQuery::new(title)
.album_title(album.as_ref().map(|a| a.album.name.as_str()))
.main_artist(Some(artists[0].name()))
};
prepared.push(QueryItem { track, query });
}
drop(db);
let progress_bar = ProgressBar::new(0, prepared.len(), progress_renderer);
let semaphore = Arc::new(Semaphore::new(10));
let mut futures: FuturesUnordered<_> = prepared
.into_iter()
.map(|q| {
let semaphore = semaphore.clone();
async move {
let _permit = semaphore.acquire().await.unwrap();
let result = q.query.run().await?;
Ok::<_, LyricSearchError>((q.track, result))
}
})
.collect();
let writer = DatabaseWriter::<LibraryDb>::spawn();
'result_loop: while let Some(result) = futures.next().await {
let (mut track, results) = result?;
if results.is_empty() {
progress_bar.set_label(&format!(
"No results for {track_title}",
track_title = track.metadata.title.as_ref().unwrap()
));
progress_bar.increment();
continue;
}
progress_bar.set_label(&format!(
"Found lyrics for {track_title}",
track_title = track.metadata.title.as_ref().unwrap()
));
progress_bar.increment();
for result in &results {
if let Some(lyrics) = result.synced_lyrics() {
track.metadata.lyric_data = Some(LyricData::Synced(lyrics));
writer.transaction(move |cas_tx| cas_tx.tx_patch(track.clone()));
continue 'result_loop;
}
}
for result in &results {
if let Some(lyrics) = result.plain_lyrics() {
if !synced_only {
track.metadata.lyric_data = Some(LyricData::Plain(lyrics));
}
writer.transaction(move |cas_tx| cas_tx.tx_patch(track.clone()));
continue 'result_loop;
}
}
track.metadata.lyric_data = Some(LyricData::Instrumental);
writer.transaction(move |cas_tx| cas_tx.tx_patch(track.clone()));
}
writer.finish()?;
Ok(())
}