Skip to main content

selene_core/library/
linking.rs

1use std::{collections::HashMap, sync::Arc};
2
3use barber::ProgressRenderer;
4use image::ImageError;
5use lunar_lib::{
6    database::{DatabaseEntry, DbHandle, TransactionError, db_transaction},
7    iterator_ext::IteratorExtensions,
8    paths::sys::sanitize_str,
9};
10use thiserror::Error;
11
12use crate::{
13    data_dir,
14    database::LibraryDb,
15    library::{
16        album::{Album, AlbumId},
17        image_art::CacheableArt,
18        linking::{album_track_linking::album_track_linking, lyric_linking::link_lrc_files},
19        track::{Track, TrackId},
20    },
21    lyrics::synced_lyrics::LyricParseError,
22};
23
24mod album_track_linking;
25mod lyric_linking;
26
27#[derive(Debug, Error)]
28pub enum LinkingError {
29    #[error("IoError: {0}")]
30    Io(#[from] std::io::Error),
31
32    #[error("ImageError: {0}")]
33    Image(#[from] ImageError),
34
35    #[error("Transaction Error: {0}")]
36    Transaction(#[from] TransactionError),
37
38    #[error("LyricParse Error: {0}")]
39    LyricParse(#[from] LyricParseError),
40}
41
42pub fn smart_link(
43    progress_renderer: Arc<dyn ProgressRenderer>,
44    db: DbHandle<LibraryDb>,
45) -> Result<(), LinkingError> {
46    let mut all_tracks: HashMap<TrackId, (Track, bool)> = Track::db_get_all(&db)?
47        .into_iter()
48        .map(|t| (t.id(), (t, false)))
49        .collect();
50    let mut all_albums: HashMap<AlbumId, (Album, bool)> = Album::db_get_all(&db)?
51        .into_iter()
52        .map(|t| (t.id(), (t, false)))
53        .collect();
54
55    link_lrc_files(all_tracks.values_mut(), progress_renderer.clone())?;
56
57    {
58        let mut track_groups: HashMap<_, Vec<_>> = HashMap::new();
59        for (track, changed) in all_tracks.values_mut() {
60            let parent = track.container().path().parent().unwrap().to_owned();
61            track_groups
62                .entry(parent)
63                .or_default()
64                .push((track, changed));
65        }
66        album_track_linking(progress_renderer, &mut all_albums, &mut track_groups)?;
67    }
68
69    for (album, changed) in all_albums.values_mut() {
70        if album.art.is_some() {
71            continue;
72        }
73
74        let first_art = album
75            .tracks
76            .iter()
77            .find_map(|tr| all_tracks.get(&tr.id)?.0.metadata().art.as_ref());
78
79        let Some(cover_art) = first_art else { continue };
80        let hash = cover_art.hash();
81
82        let all_same = album
83            .tracks
84            .iter()
85            .filter_map(|tr| all_tracks.get(&tr.id))
86            .all(|(t, _)| t.metadata().art.as_ref().is_some_and(|a| a.hash() == hash));
87
88        if all_same {
89            let export_path = data_dir().join(format!("album_art/{}", sanitize_str(&album.name())));
90            album.art = Some(cover_art.export_to_image_art(&export_path)?);
91            *changed = true;
92        }
93    }
94
95    let changed_tracks = all_tracks
96        .into_iter()
97        .filter_map(|(_, (track, changed))| changed.then_some(track))
98        .to_vec();
99    let changed_albums = all_albums
100        .into_iter()
101        .filter_map(|(_, (album, changed))| changed.then_some(album))
102        .to_vec();
103
104    db_transaction(
105        |cas_tx| {
106            for track in &changed_tracks {
107                cas_tx.tx_upsert(track.clone())?;
108            }
109            for album in &changed_albums {
110                cas_tx.tx_upsert(album.clone())?;
111            }
112            Ok(())
113        },
114        db,
115        false,
116    )
117    .map_err(TransactionError::from)?;
118
119    Ok(())
120}