selene_core/library/
linking.rs1use 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}