Skip to main content

selene_core/library/
extract.rs

1use std::{
2    collections::{HashMap, hash_map::Entry},
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use blake3::hash;
8use lofty::{
9    file::TaggedFileExt,
10    picture::PictureType,
11    tag::{ItemKey, TagItem, TagType},
12};
13use lunar_lib::database::{DatabaseEntry, writer::DatabaseWriter};
14
15use barber::{ProgressBar, ProgressRenderer};
16use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
17
18use crate::{
19    database::{LibraryDb, Patchable, tx_extensions::CasTxExtensions},
20    errors::ExtractError,
21    library::{
22        album::{Album, TrackReference, UNKNOWN_ALBUM},
23        artist::{Artist, ArtistGroup, ArtistId},
24        hash_source_files,
25        metadata::{LoftyTagExtensions, extract_instrumental},
26        track::{
27            Track, TrackId, cover_art::CoverArt, lyric_data::LyricData, track_meta::TrackMeta,
28        },
29    },
30    symphonia_helpers::{ContainerExtractResult, extract_from_file},
31    utils::hash_file,
32};
33
34#[derive(Debug, Clone)]
35pub struct ExtractResult {
36    pub track: Track,
37    pub album: Option<Album>,
38    pub artists: Vec<Artist>,
39}
40
41/// Filters a slice of [`TrackId`]'s using the input
42pub fn find_needs_extract(
43    progress_renderer: Arc<dyn ProgressRenderer>,
44) -> Result<Vec<PathBuf>, ExtractError> {
45    let sources = hash_source_files(progress_renderer)?;
46    let known_tracks = Track::db_get_all()?;
47    let known_track_ids: Vec<TrackId> = known_tracks.iter().map(Track::id).collect();
48
49    Ok(sources
50        .into_iter()
51        .filter_map(|(id, path)| (!known_track_ids.contains(&id)).then_some(path))
52        .collect())
53}
54
55pub fn extract(
56    files: &[PathBuf],
57    progress_renderer: Arc<dyn ProgressRenderer>,
58    dry: bool,
59) -> Result<(), ExtractError> {
60    let files: Vec<_> = files.iter().collect();
61
62    if files.is_empty() {
63        return Ok(());
64    }
65
66    let progress_bar = ProgressBar::new(0, files.len(), progress_renderer);
67    progress_bar.set_label("Extracting metadata from files...");
68
69    let writer = DatabaseWriter::<LibraryDb>::spawn();
70
71    files
72        .par_iter()
73        .try_for_each(|source| -> Result<(), ExtractError> {
74            if writer.is_closed() {
75                return Ok(());
76            }
77
78            let ExtractResult {
79                track,
80                album,
81                artists,
82            } = extract_metadata(source)?;
83
84            if writer.is_closed() {
85                return Ok(());
86            }
87
88            if !dry {
89                writer.transaction(move |cas_tx| {
90                    cas_tx.tx_patch(track.clone())?;
91
92                    if let Some(album) = &album {
93                        cas_tx.tx_patch(album.clone())?;
94                    }
95
96                    for artist in &artists {
97                        cas_tx.tx_patch(artist.clone())?;
98                    }
99
100                    Ok(())
101                });
102            }
103
104            progress_bar.set_label(&format!(
105                "Extracted metadata from '{path}'",
106                path = source.display()
107            ));
108            progress_bar.increment();
109
110            Ok(())
111        })?;
112
113    writer.finish()?;
114
115    progress_bar.flush();
116    Ok(())
117}
118
119/// Analyzes and extracts the input file metadata into an [`ExtractResult`]
120pub fn extract_metadata(source_file: impl AsRef<Path>) -> Result<ExtractResult, ExtractError> {
121    let source_file = source_file.as_ref();
122
123    let ContainerExtractResult {
124        container,
125        // TODO: Replace lofty reading with symphonia
126        metadata: _metadata,
127    } = extract_from_file(source_file)?;
128
129    let mut metadata = lofty::read_from_path(source_file)?;
130    let tags = metadata.primary_tag_mut().unwrap();
131
132    let (title, track_artists) = tags.track_title_and_artists();
133
134    let (title, instrumental) = match title {
135        Some(t) => {
136            let (extracted, instrumental) = extract_instrumental(&t);
137            (Some(extracted.into_owned()), Some(instrumental))
138        }
139        None => (None, None),
140    };
141
142    let mut all_artists: HashMap<ArtistId, Artist> =
143        track_artists.iter().cloned().map(|a| (a.id(), a)).collect();
144
145    let date = tags.date();
146
147    let track_num = tags.track_num();
148    let disc_num = tags.disc_num();
149
150    let genre: Vec<String> = tags.take_strings(ItemKey::Genre).collect();
151
152    let mut album = {
153        let (album_title, album_artists) = tags.album_title_and_artists();
154        let track_total = tags.track_total();
155        let disc_total = tags.disc_total();
156
157        let has_album = has_album(
158            title.as_deref(),
159            album_title.as_deref(),
160            &track_artists,
161            &album_artists,
162            track_num,
163            track_total,
164            disc_num,
165            disc_total,
166        );
167
168        if has_album {
169            let mut album = Album::new(
170                album_title.unwrap_or(UNKNOWN_ALBUM.to_owned()),
171                ArtistGroup::from_artists(&album_artists),
172                Vec::new(),
173            );
174
175            for mut artist in album_artists {
176                artist.albums.push(album.id());
177
178                match all_artists.entry(artist.id()) {
179                    Entry::Occupied(mut entry) => entry.get_mut().patch(artist),
180                    Entry::Vacant(entry) => {
181                        entry.insert(artist);
182                    }
183                }
184            }
185
186            album.date = date;
187            album.track_total = track_total;
188            album.disc_total = disc_total;
189            album.genre = genre.clone();
190            Some(album)
191        } else {
192            None
193        }
194    };
195
196    let lyrics = tags.lyrics();
197    let lyrics = if instrumental == Some(true) {
198        Some(LyricData::Instrumental)
199    } else {
200        lyrics
201    };
202
203    let other = tags
204        .items()
205        .cloned()
206        .map(TagItem::consume)
207        .filter_map(|(k, v)| {
208            let v = v.text()?.to_owned();
209            let k = k.map_key(TagType::VorbisComments)?.to_owned();
210
211            Some((k, v))
212        });
213
214    let art = tags
215        .get_picture_type(PictureType::CoverFront)
216        .or(tags.pictures().first())
217        .map(|p| CoverArt::Embedded {
218            hash: hash(p.data()),
219            source: source_file.to_path_buf(),
220        });
221
222    let metadata = TrackMeta {
223        album: album.as_ref().map(Album::id),
224        artists: ArtistGroup::from_artists(&track_artists),
225        date,
226        genre,
227        lyric_data: lyrics,
228        other: other.collect(),
229        title,
230        art,
231    };
232
233    let track = Track::new(hash_file(source_file)?, container, metadata);
234
235    if let Some(album) = &mut album {
236        album.tracks.push(TrackReference {
237            id: track.id(),
238            track_num,
239            disc_num,
240        });
241    }
242
243    let mut all_artists: Vec<Artist> = all_artists.into_values().collect();
244
245    for artist in &mut all_artists {
246        if track_artists.contains(artist) {
247            artist.tracks.push(track.id());
248        }
249    }
250
251    let result = ExtractResult {
252        track,
253        album,
254        artists: all_artists,
255    };
256
257    Ok(result)
258}
259
260fn has_album(
261    track_title: Option<&str>,
262    album_title: Option<&str>,
263    track_artists: &[Artist],
264    album_artists: &[Artist],
265    track_num: Option<u32>,
266    track_total: Option<u32>,
267    disc_num: Option<u32>,
268    disc_total: Option<u32>,
269) -> bool {
270    if track_total == Some(1) && disc_total.is_none_or(|d| d <= 1) {
271        return false;
272    }
273
274    // Returns true if the album name != track name
275    if album_title.zip(track_title).is_none_or(|(a, b)| a != b) {
276        return true;
277    }
278
279    // Returns true if the album artists are not the same as the track artists
280    if *album_artists != *track_artists {
281        return true;
282    }
283
284    // Returns true if the track/disc value or total is greater is some and is greater than 1
285    track_num.is_some_and(|v| v > 1)
286        || track_total.is_some_and(|v| v > 1)
287        || disc_num.is_some_and(|v| v > 1)
288        || disc_total.is_some_and(|v| v > 1)
289}