1use std::{
2 collections::{HashMap, hash_map::Entry},
3 convert::Infallible,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use blake3::hash;
9use lofty::{
10 file::TaggedFileExt,
11 picture::PictureType,
12 tag::{ItemKey, TagItem, TagType},
13};
14use lunar_lib::{
15 database::{
16 CompareAndSwapTransaction, CustomTransactionError, DatabaseEntry, DbHandle,
17 TransactionError, db_transaction, writer::DatabaseWriter,
18 },
19 vec_ext::VecExtensions,
20};
21
22use barber::{ProgressBar, ProgressRenderer};
23use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
24use thiserror::Error;
25
26use crate::{
27 config::common_config,
28 database::LibraryDb,
29 library::{
30 album::{Album, TrackReference},
31 artist::{Artist, ArtistId},
32 hash_source_files,
33 metadata::{LoftyTagTakeAccessors, extract_instrumental},
34 track::{
35 ResolvedTrack, Track, TrackId, cover_art::CoverArt, lyric_data::LyricData,
36 track_meta::TrackMeta,
37 },
38 },
39 media_container::ContainerError,
40 symphonia_helpers::{ContainerExtractResult, extract_from_file},
41};
42
43#[derive(Debug, Error)]
44pub enum ExtractError {
45 #[error("IoError: {0}")]
46 Io(#[from] std::io::Error),
47
48 #[error("LoftyError: {0}")]
49 Lofty(#[from] lofty::error::LoftyError),
50
51 #[error("DatabaseError: {0}")]
52 Database(#[from] lunar_lib::database::DatabaseError),
53
54 #[error("Transaction Error: {0}")]
55 Transaction(#[from] TransactionError),
56
57 #[error("Container Error: {0}")]
58 Container(#[from] ContainerError),
59
60 #[error("Template Error: {0}")]
61 Template(#[from] lunar_lib::formatter::TemplateError),
62
63 #[error("InvalidContainer Error")]
64 InvalidContainer,
65}
66
67pub fn find_needs_extract(
69 progress_renderer: Arc<dyn ProgressRenderer>,
70 db: &LibraryDb,
71) -> Result<Vec<PathBuf>, ExtractError> {
72 let sources = hash_source_files(progress_renderer)?;
73 let known_tracks = Track::db_get_all(db)?;
74 let known_track_ids: Vec<TrackId> = known_tracks.iter().map(Track::id).collect();
75
76 Ok(sources
77 .into_iter()
78 .filter_map(|(id, path)| (!known_track_ids.contains(&id)).then_some(path))
79 .collect())
80}
81
82pub fn extract(
83 progress_renderer: Arc<dyn ProgressRenderer>,
84 dry: bool,
85) -> Result<(), ExtractError> {
86 let db = DbHandle::<LibraryDb>::open()?;
87 let files = find_needs_extract(progress_renderer.clone(), &db)?;
88
89 if files.is_empty() {
90 return Ok(());
91 }
92
93 let progress_bar = ProgressBar::new(0, files.len(), progress_renderer);
94 progress_bar.set_label("Extracting metadata from files...");
95
96 #[inline]
97 fn transaction_fn(
98 cas_tx: &mut CompareAndSwapTransaction<LibraryDb>,
99 result: &ResolvedTrack,
100 ) -> Result<(), TransactionError> {
101 cas_tx.tx_upsert((*result.track).clone())?;
102
103 if let Some((album, artists, ..)) = result.album_info() {
104 cas_tx.tx_patch((**album).clone())?;
105 for artist in artists {
106 cas_tx.tx_patch((**artist).clone())?;
107 }
108 }
109
110 for artist in result.artists() {
111 cas_tx.tx_patch((**artist).clone())?;
112 }
113
114 Ok(())
115 }
116
117 if common_config().main.multithreading {
118 let writer = DatabaseWriter::<LibraryDb>::spawn();
119 files
120 .par_iter()
121 .try_for_each(|source| -> Result<(), ExtractError> {
122 if writer.is_closed() {
123 return Ok(());
124 }
125
126 let result = extract_metadata(source)?;
127
128 if writer.is_closed() {
129 return Ok(());
130 }
131
132 if !dry {
133 writer.transaction(move |cas_tx| transaction_fn(cas_tx, &result));
134 }
135
136 progress_bar.set_label(&format!(
137 "Extracted metadata from '{path}'",
138 path = source.display()
139 ));
140 progress_bar.increment();
141
142 Ok(())
143 })?;
144 writer.finish()?;
145 } else {
146 for source in files {
147 let result = extract_metadata(&source)?;
148
149 if !dry {
150 db_transaction(
151 |cas_tx| -> Result<(), CustomTransactionError<Infallible>> {
152 transaction_fn(cas_tx, &result)?;
153 Ok(())
154 },
155 DbHandle::<LibraryDb>::open().unwrap(),
156 false,
157 )
158 .map_err(TransactionError::from)?;
159 }
160
161 progress_bar.set_label(&format!(
162 "Extracted metadata from '{path}'",
163 path = source.display()
164 ));
165 progress_bar.increment();
166 }
167 }
168
169 progress_bar.flush();
170 Ok(())
171}
172
173pub fn extract_metadata(source_file: impl AsRef<Path>) -> Result<ResolvedTrack, ExtractError> {
175 let source_file = source_file.as_ref();
176
177 let ContainerExtractResult {
178 container,
179 metadata: _metadata,
181 } = extract_from_file(source_file)?;
182
183 let mut metadata = lofty::read_from_path(source_file)?;
184 let tags = metadata.primary_tag_mut().unwrap();
185
186 let (title, track_artists) = tags.track_title_and_artists();
187
188 let (title, instrumental) = match title {
189 Some(t) => {
190 let (extracted, instrumental) = extract_instrumental(&t);
191 (Some(extracted.into_owned()), Some(instrumental))
192 }
193 None => (None, None),
194 };
195
196 let mut all_artists: HashMap<ArtistId, Artist> =
197 track_artists.iter().cloned().map(|a| (a.id(), a)).collect();
198
199 let date = tags.date();
200
201 let track_num = tags.track_num();
202 let disc_num = tags.disc_num();
203
204 let genre: Vec<String> = tags.take_strings(ItemKey::Genre).collect();
205
206 let mut album = {
207 let (album_title, album_artists) = tags.album_title_and_artists();
208 let track_total = tags.track_total();
209 let disc_total = tags.disc_total();
210
211 if let Some(album_title) = album_title
212 && has_album(
213 title.as_deref(),
214 &album_title,
215 &track_artists,
216 &album_artists,
217 track_num,
218 track_total,
219 disc_num,
220 disc_total,
221 )
222 {
223 let mut album = Album::new(
224 album_title,
225 album_artists.iter().map(Artist::id).collect(),
226 Vec::new(),
227 );
228
229 for mut artist in album_artists {
230 artist.albums.push(album.id());
231
232 match all_artists.entry(artist.id()) {
233 Entry::Occupied(mut entry) => {
234 entry.get_mut().albums.extend_unique(artist.albums);
235 }
236 Entry::Vacant(entry) => {
237 entry.insert(artist);
238 }
239 }
240 }
241
242 album.date = date;
243 album.track_total = track_total;
244 album.disc_total = disc_total;
245 album.genre = genre.clone();
246 Some(album)
247 } else {
248 None
249 }
250 };
251
252 let lyrics = tags.lyrics();
253 let lyrics = if instrumental == Some(true) {
254 Some(LyricData::Instrumental)
255 } else {
256 lyrics
257 };
258
259 let other = tags
260 .items()
261 .cloned()
262 .map(TagItem::consume)
263 .filter_map(|(k, v)| {
264 let v = v.text()?.to_owned();
265 let k = k.map_key(TagType::VorbisComments)?.to_owned();
266
267 Some((k, v))
268 });
269
270 let art = tags
271 .get_picture_type(PictureType::CoverFront)
272 .or(tags.pictures().first())
273 .map(|p| CoverArt::Embedded {
274 hash: hash(p.data()),
275 source: source_file.to_path_buf(),
276 });
277
278 let metadata = TrackMeta {
279 album: album.as_ref().map(Album::id),
280 artists: track_artists.iter().map(Artist::id).collect(),
281 date,
282 genre,
283 lyric_data: lyrics,
284 other: other.collect(),
285 title,
286 art,
287 };
288
289 let track = Track::new(container, metadata)?;
290
291 if let Some(album) = &mut album {
292 album.tracks.push(TrackReference {
293 id: track.id(),
294 track_num,
295 disc_num,
296 });
297 }
298
299 for artist in all_artists.values_mut() {
300 if track_artists.contains(artist) {
301 artist.tracks.push(track.id());
302 }
303 }
304
305 let all_artists: HashMap<_, _> = all_artists
306 .into_iter()
307 .map(|(id, a)| (id, Arc::new(a)))
308 .collect();
309
310 let artists = track_artists
311 .iter()
312 .map(|a| all_artists[&a.id()].clone())
313 .collect();
314
315 let album_artists = album.as_ref().map(|album| {
316 album
317 .artists
318 .iter()
319 .map(|id| all_artists[id].clone())
320 .collect()
321 });
322
323 Ok(ResolvedTrack {
324 track: Arc::new(track),
325 album: album.map(Arc::new),
326 artists,
327 album_artists,
328 })
329}
330
331fn has_album(
332 track_title: Option<&str>,
333 album_title: &str,
334 track_artists: &[Artist],
335 album_artists: &[Artist],
336 track_num: Option<u32>,
337 track_total: Option<u32>,
338 disc_num: Option<u32>,
339 disc_total: Option<u32>,
340) -> bool {
341 if track_total == Some(1) && disc_total.is_none_or(|d| d <= 1) {
342 return false;
343 }
344
345 if track_title.is_none_or(|t| t != album_title) {
347 return true;
348 }
349
350 if *album_artists != *track_artists {
352 return true;
353 }
354
355 track_num.is_some_and(|v| v > 1)
357 || track_total.is_some_and(|v| v > 1)
358 || disc_num.is_some_and(|v| v > 1)
359 || disc_total.is_some_and(|v| v > 1)
360}