Skip to main content

selene_core/library/track/
core_impls.rs

1use std::{
2    collections::HashMap,
3    fs, io,
4    path::{Path, PathBuf},
5};
6
7use blake3::Hash;
8use lunar_lib::formatter::{FormatError, FormatTable, format_str};
9use thiserror::Error;
10
11use crate::{
12    config::common::{LoudnormConfig, common_config},
13    database::{CompareAndSwapTransaction, DatabaseEntry, DatabaseError},
14    errors::{LibraryError, MetadataError},
15    library::{
16        album::Album,
17        artist::{Artist, ArtistGroup, add_from_artists},
18        metadata::{
19            ALBUM_ARTIST_KEY, ALBUM_KEY, ARTIST_KEY, DATE_KEY, DISC_NUM_KEY, GENRE_KEY,
20            MetadataKey, TITLE_KEY, TRACK_NUM_KEY,
21        },
22        track::{
23            Track, TrackId,
24            lyric_data::LyricData,
25            track_meta::{TrackAlbumInfo, TrackMeta},
26        },
27    },
28    media_container::MediaContainer,
29    utils::pair_extension,
30};
31
32// Core
33impl Track {
34    #[must_use]
35    pub fn new(
36        hash: Hash,
37        src_container: MediaContainer,
38        metadata: TrackMeta,
39        relative_path: PathBuf,
40    ) -> Self {
41        Self {
42            id: TrackId::new(hash),
43            src_container,
44            lib_container: None,
45            relative_library_path: relative_path,
46            metadata,
47            loudnorm_analysis: None,
48            applied_loudnorm: None,
49            version: Track::VERSION_NUMBER,
50        }
51    }
52}
53
54// Accessors
55impl Track {
56    #[must_use]
57    pub fn id(&self) -> TrackId {
58        self.id
59    }
60
61    #[must_use]
62    pub fn loudnorm(&self) -> Option<&LoudnormConfig> {
63        self.applied_loudnorm.as_ref()
64    }
65
66    #[must_use]
67    pub fn src_container(&self) -> &MediaContainer {
68        &self.src_container
69    }
70
71    #[must_use]
72    pub fn lib_container(&self) -> Option<&MediaContainer> {
73        self.lib_container.as_ref()
74    }
75
76    pub fn album(&self) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
77        if let Some(album_id) = self.metadata.album {
78            Ok(Some({
79                let album = Album::db_get(album_id)?.expect("Dangling album reference");
80
81                let reference = album
82                    .track_refs()
83                    .iter()
84                    .find(|t| t.id == self.id)
85                    .expect("Track not found in album");
86
87                let track_num = reference.track_num;
88                let disc_num = reference.disc_num;
89
90                TrackAlbumInfo {
91                    album,
92                    track_num,
93                    disc_num,
94                }
95            }))
96        } else {
97            Ok(None)
98        }
99    }
100
101    pub fn tx_album(
102        &self,
103        cas_tx: &mut CompareAndSwapTransaction,
104    ) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
105        if let Some(album_id) = self.metadata.album {
106            Ok(Some({
107                let album = cas_tx.tx_get(album_id)?.expect("Dangling album reference");
108
109                let reference = album
110                    .track_refs()
111                    .iter()
112                    .find(|t| t.id == self.id)
113                    .expect("Dangling album>track reference");
114
115                let track_num = reference.track_num;
116                let disc_num = reference.disc_num;
117
118                TrackAlbumInfo {
119                    album,
120                    track_num,
121                    disc_num,
122                }
123            }))
124        } else {
125            Ok(None)
126        }
127    }
128}
129
130// Mutators
131impl Track {
132    pub fn metadata_key_values(&self) -> Result<HashMap<String, String>, DatabaseError> {
133        let mut map = HashMap::new();
134
135        if let Some(track_album_info) = self.album()? {
136            map.insert(ALBUM_KEY, track_album_info.album.name.clone());
137
138            let artists = track_album_info
139                .album
140                .artists()
141                .artists()?
142                .iter()
143                .map(Artist::name)
144                .collect::<Vec<_>>()
145                .join(";");
146            map.insert(ALBUM_ARTIST_KEY, artists);
147
148            if let Some(track_num) = track_album_info.track_num
149                && let Some(track_total) = track_album_info.album.track_total
150            {
151                map.insert(TRACK_NUM_KEY, format!("{track_num}/{track_total}"));
152            }
153
154            if let Some(disc_num) = track_album_info.disc_num
155                && let Some(disc_total) = track_album_info.album.disc_total
156            {
157                map.insert(DISC_NUM_KEY, format!("{disc_num}/{disc_total}"));
158            }
159        }
160
161        let artists = self
162            .metadata
163            .artists
164            .artists()?
165            .iter()
166            .map(Artist::name)
167            .collect::<Vec<_>>()
168            .join(";");
169        map.insert(ARTIST_KEY, artists);
170
171        if let Some(date) = self.metadata.date {
172            map.insert(DATE_KEY, date.to_string());
173        }
174
175        if let Some(genre) = &self.metadata.genre {
176            map.insert(GENRE_KEY, genre.to_owned());
177        }
178
179        if let Some(title) = &self.metadata.title {
180            map.insert(TITLE_KEY, title.to_owned());
181        }
182
183        if let Some(lyric_data) = &self.metadata.lyric_data {
184            let (k, v) = lyric_data.get_metadata_value();
185            map.insert(k, v);
186        }
187
188        let mut collected = self.metadata.other.clone();
189        collected.extend(map.into_iter().map(|(k, v)| (k.to_owned(), v)));
190
191        Ok(collected)
192    }
193
194    pub fn tx_apply_metadata_key(
195        &self,
196        key: MetadataKey,
197        cas_tx: &mut CompareAndSwapTransaction,
198    ) -> Result<(), MetadataError> {
199        let mut track = cas_tx
200            .tx_get(self.id())?
201            .ok_or(DatabaseError::MissingEntry)?;
202
203        match key {
204            MetadataKey::Album(v) => {
205                cas_tx.tracks_set_album(v.as_ref().map(Album::id), std::iter::once(&track.id()))?;
206            }
207            MetadataKey::Artist(v) => track.metadata.artists = ArtistGroup::from_artists(&v),
208            MetadataKey::Date(v) => track.metadata.date = v,
209            MetadataKey::DiscNum(v) => {
210                if let Some(album_id) = track.metadata.album {
211                    let mut album = cas_tx.tx_get(album_id)?.expect("Dangling album reference");
212
213                    let track_reference = album
214                        .tracks
215                        .iter_mut()
216                        .find(|t| t.id == track.id())
217                        .expect("Dangling album>track reference");
218
219                    track_reference.disc_num = v;
220
221                    cas_tx.tx_upsert(album_id, Some(album))?;
222                } else {
223                    return Err(MetadataError::MissingAlbum(
224                        "disc cannot be set because track has no album".to_owned(),
225                    ));
226                }
227            }
228            MetadataKey::Genre(v) => track.metadata.genre = v,
229            MetadataKey::Lyrics(v) => track.metadata.lyric_data = v,
230            MetadataKey::Instrumental(v) => {
231                track.metadata.lyric_data = v.then_some(LyricData::Instrumental);
232            }
233            MetadataKey::Title(v) => track.metadata.title = v,
234            MetadataKey::TrackNum(v) => {
235                if let Some(album_id) = track.metadata.album {
236                    let mut album = cas_tx.tx_get(album_id)?.expect("Dangling album reference");
237
238                    let track_reference = album
239                        .tracks
240                        .iter_mut()
241                        .find(|t| t.id == track.id())
242                        .expect("Dangling album>track reference");
243
244                    track_reference.track_num = v;
245
246                    cas_tx.tx_upsert(album_id, Some(album))?;
247                } else {
248                    return Err(MetadataError::MissingAlbum(
249                        "track cannot be set because track has no album".to_owned(),
250                    ));
251                }
252            }
253            MetadataKey::Other(key, v) => {
254                if let Some(v) = v {
255                    track.metadata.other.insert(key, v);
256                } else {
257                    track.metadata.other.remove(&key);
258                }
259            }
260            _ => {
261                return Err(MetadataError::KeyNotAllowed(format!(
262                    "{key} cannot be used on track metadata",
263                    key = key.to_key()
264                )));
265            }
266        }
267
268        cas_tx.tx_upsert(track.id(), Some(track))?;
269
270        Ok(())
271    }
272}
273
274impl Track {
275    /// Migrates a track from its relative path to the input library directory
276    ///
277    /// # Errors
278    ///
279    /// This function will error if:
280    /// - The track does not exist in the library.
281    /// - [`std::fs::rename()`] fails.
282    /// - The track fails to patch using [`DatabaseEntry::db_patch()`].
283    ///
284    /// The database is patched AFTER the file is renamed, if the file renames succesfully, but the patch fails, this could lead to a minor desync between the internal storage and the filesystem.
285    /// This can easily be revalidated via orphan relinking
286    pub fn migrate(&mut self, library_dir: impl AsRef<Path>) -> Result<(), TrackRenameError> {
287        let Some(lib_container) = &mut self.lib_container else {
288            return Err(io::Error::new(
289                io::ErrorKind::NotFound,
290                "A library container was not found for the input track".to_string(),
291            )
292            .into());
293        };
294
295        let absolute_path = library_dir.as_ref().join(&self.relative_library_path);
296
297        fs::create_dir_all(absolute_path.parent().expect("File cannot be root"))?;
298        if let Err(err) = fs::rename(lib_container.path(), &absolute_path) {
299            match err.kind() {
300                io::ErrorKind::NotFound if fs::symlink_metadata(&absolute_path)?.is_file() => {}
301                _ => return Err(err.into()),
302            }
303        }
304
305        lib_container.set_path(absolute_path);
306        self.db_patch()?;
307        Ok(())
308    }
309}
310
311#[derive(Debug, Error)]
312pub enum TrackRenameError {
313    #[error("IoError: {0}")]
314    Io(#[from] std::io::Error),
315
316    #[error("DatabaseError: {0}")]
317    Database(#[from] DatabaseError),
318
319    #[error("FormatError: {0}")]
320    Format(#[from] FormatError),
321
322    #[error("LibraryError: {0}")]
323    Library(#[from] LibraryError),
324
325    #[error("{0}")]
326    ConflictingNames(String),
327}
328
329/// Calculates the relative path for a track.
330///
331/// # Errors
332///
333/// Returns an error if database references cannot be obtained
334pub fn calculate_rel_path(
335    metadata: &TrackMeta,
336    container_ref: &MediaContainer,
337) -> Result<PathBuf, TrackRenameError> {
338    let artists = metadata.artists.artists()?;
339    let album = metadata.album()?;
340
341    let mut format_table = FormatTable::new();
342    format_table.extend_from_taggable(metadata);
343    add_from_artists(&mut format_table, &artists, "track");
344    if let Some(album) = album {
345        format_table.extend_from_taggable(&album);
346    }
347
348    let mut path = PathBuf::from(format_str(
349        &common_config().track_name_config.format_string,
350        &format_table,
351    )?);
352
353    path.add_extension(
354        pair_extension(container_ref.container(), container_ref.codec())
355            .expect("Invalid container/codec pair when renaming"),
356    );
357
358    Ok(path)
359}