Skip to main content

selene_core/library/track/
core_impls.rs

1use std::{
2    fs, io,
3    path::{Path, PathBuf},
4};
5
6use blake3::Hash;
7use chrono::{Datelike, Timelike};
8use lofty::{
9    ogg::VorbisComments,
10    tag::{Accessor, Tag, TagType, items::Timestamp},
11};
12use lunar_lib::{
13    database::{
14        CompareAndSwapTransaction, Database, DatabaseEntry, DatabaseError, TransactionError,
15    },
16    formatter::{FormatError, FormatTable, format_str},
17};
18use thiserror::Error;
19
20use crate::{
21    config::common::{LoudnormSettings, common_config},
22    database::{LibraryDb, entry_extensions::EntryExtensions},
23    errors::{LibraryError, MetadataError},
24    library::{
25        album::Album,
26        artist::{Artist, add_from_artists},
27        track::{
28            Track, TrackId,
29            track_meta::{TrackAlbumInfo, TrackMeta},
30        },
31    },
32    media_container::{ContainerFormat, MediaContainer},
33};
34
35use super::lyric_data::LyricData;
36
37// Core
38impl Track {
39    #[must_use]
40    pub fn new(
41        hash: Hash,
42        src_container: MediaContainer,
43        metadata: TrackMeta,
44        relative_library_path: PathBuf,
45    ) -> Self {
46        Self {
47            id: TrackId::new(hash),
48            src_container,
49            lib_container: None,
50            relative_library_path,
51            metadata,
52            loudnorm_analysis: None,
53            applied_loudnorm: None,
54            version: Track::VERSION_NUMBER,
55        }
56    }
57}
58
59// Accessors
60impl Track {
61    #[must_use]
62    pub fn id(&self) -> TrackId {
63        self.id
64    }
65
66    #[must_use]
67    pub fn loudnorm(&self) -> Option<&LoudnormSettings> {
68        self.applied_loudnorm.as_ref()
69    }
70
71    #[must_use]
72    pub fn src_container(&self) -> &MediaContainer {
73        &self.src_container
74    }
75
76    #[must_use]
77    pub fn lib_container(&self) -> Option<&MediaContainer> {
78        self.lib_container.as_ref()
79    }
80
81    pub fn album(&self, db: &LibraryDb) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
82        if let Some(album_id) = self.metadata.album {
83            Ok(Some({
84                let album = Album::db_get_from(album_id, db)?.expect("Dangling album reference");
85
86                let reference = album
87                    .track_refs()
88                    .iter()
89                    .find(|t| t.id == self.id)
90                    .expect("Track not found in album");
91
92                let track_num = reference.track_num;
93                let disc_num = reference.disc_num;
94
95                TrackAlbumInfo {
96                    album,
97                    track_num,
98                    disc_num,
99                }
100            }))
101        } else {
102            Ok(None)
103        }
104    }
105
106    pub fn tx_album(
107        &self,
108        cas_tx: &mut CompareAndSwapTransaction<LibraryDb>,
109    ) -> Result<Option<TrackAlbumInfo>, TransactionError> {
110        if let Some(album_id) = self.metadata.album {
111            Ok(Some({
112                let album = cas_tx.tx_get(album_id)?.expect("Dangling album reference");
113
114                let reference = album
115                    .track_refs()
116                    .iter()
117                    .find(|t| t.id == self.id)
118                    .expect("Dangling album>track reference");
119
120                let track_num = reference.track_num;
121                let disc_num = reference.disc_num;
122
123                TrackAlbumInfo {
124                    album,
125                    track_num,
126                    disc_num,
127                }
128            }))
129        } else {
130            Ok(None)
131        }
132    }
133}
134
135// Mutators
136impl Track {
137    pub fn metadata_key_values(
138        &self,
139        format: &ContainerFormat,
140    ) -> Result<Vec<(String, String)>, MetadataError> {
141        let mut tags = VorbisComments::new();
142        {
143            let db = LibraryDb::open()?;
144            if let Some(track_album_info) = self.album(&db)? {
145                tags.set_album(track_album_info.album.name.clone());
146
147                let artists = track_album_info
148                    .album
149                    .artists()
150                    .artists(&db)?
151                    .iter()
152                    .map(Artist::name)
153                    .collect::<Vec<_>>()
154                    .join(";");
155                tags.insert("ALBUMARTIST".to_owned(), artists);
156
157                if let Some(track_num) = track_album_info.track_num {
158                    tags.set_track(track_num);
159                    if let Some(track_total) = track_album_info.album.track_total {
160                        tags.set_track_total(track_total);
161                    }
162                }
163
164                if let Some(disc_num) = track_album_info.disc_num {
165                    tags.set_disk(disc_num);
166                    if let Some(disc_total) = track_album_info.album.disc_total {
167                        tags.set_disk_total(disc_total);
168                    }
169                }
170            }
171
172            let artists = self
173                .metadata
174                .artists
175                .artists(&db)?
176                .iter()
177                .map(Artist::name)
178                .collect::<Vec<_>>()
179                .join(";");
180            tags.set_artist(artists);
181        }
182
183        if let Some(date) = self.metadata.date {
184            let ts = Timestamp {
185                year: date.year() as u16,
186                month: Some(date.month() as u8),
187                day: Some(date.day() as u8),
188                hour: Some(date.hour() as u8),
189                minute: Some(date.minute() as u8),
190                second: Some(date.second() as u8),
191            };
192            tags.set_date(ts);
193        }
194
195        for genre in &self.metadata.genre {
196            tags.push("GENRE".to_owned(), genre.to_owned());
197        }
198
199        if let Some(title) = &self.metadata.title {
200            tags.set_title(title.to_owned());
201        }
202
203        if let Some(lyric_data) = &self.metadata.lyric_data {
204            match lyric_data {
205                LyricData::Instrumental => {
206                    tags.insert("INSTRUMENTAL".to_owned(), "1".to_owned());
207                }
208                LyricData::Plain(lyrics) => {
209                    tags.insert("UNSYNCEDLYRICS".to_owned(), lyrics.to_string());
210                }
211                LyricData::Synced(lyrics) => {
212                    tags.insert("SYNCEDLYRICS".to_owned(), lyrics.to_lrc_string());
213                }
214            }
215        }
216
217        self.metadata.other.iter().for_each(|(k, v)| {
218            tags.insert(k.to_uppercase(), v.to_owned());
219        });
220
221        let _ = tags.remove("ENCODEDBY");
222
223        let tag_type = match format {
224            ContainerFormat::Flac | ContainerFormat::Ogg => TagType::VorbisComments,
225            ContainerFormat::Mpa | ContainerFormat::Wav => TagType::Id3v2,
226            ContainerFormat::Aiff => TagType::AiffText,
227            ContainerFormat::Ape => TagType::Ape,
228        };
229
230        let mut tags: Tag = tags.into();
231        tags.re_map(tag_type);
232
233        let tags = tags
234            .items()
235            .map(|tag| {
236                (
237                    tag.key().map_key(tag_type).unwrap().to_owned(),
238                    tag.value().text().unwrap().to_owned(),
239                )
240            })
241            .collect();
242
243        Ok(tags)
244    }
245}
246
247impl Track {
248    /// Migrates a track from its relative path to the input library directory
249    ///
250    /// # Errors
251    ///
252    /// This function will error if:
253    /// - The track does not exist in the library.
254    /// - [`std::fs::rename()`] fails.
255    /// - The track fails to patch using [`DatabaseEntry::db_patch()`].
256    ///
257    /// 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.
258    /// This can easily be revalidated via orphan relinking
259    pub fn migrate(&mut self, library_dir: impl AsRef<Path>) -> Result<(), TrackRenameError> {
260        let Some(lib_container) = &mut self.lib_container else {
261            return Err(io::Error::new(
262                io::ErrorKind::NotFound,
263                "A library container was not found for the input track".to_string(),
264            )
265            .into());
266        };
267
268        let absolute_path = library_dir.as_ref().join(&self.relative_library_path);
269
270        fs::create_dir_all(absolute_path.parent().expect("File cannot be root"))?;
271        if let Err(err) = fs::rename(lib_container.path(), &absolute_path) {
272            match err.kind() {
273                io::ErrorKind::NotFound if fs::symlink_metadata(&absolute_path)?.is_file() => {}
274                _ => return Err(err.into()),
275            }
276        }
277
278        lib_container.set_path(absolute_path);
279
280        Track::db_patch(self.clone(), None)?;
281
282        Ok(())
283    }
284}
285
286#[derive(Debug, Error)]
287pub enum TrackRenameError {
288    #[error("IoError: {0}")]
289    Io(#[from] std::io::Error),
290
291    #[error("DatabaseError: {0}")]
292    Database(#[from] DatabaseError),
293
294    #[error("TransactionError: {0}")]
295    Transaction(#[from] TransactionError),
296
297    #[error("FormatError: {0}")]
298    Format(#[from] FormatError),
299
300    #[error("LibraryError: {0}")]
301    Library(#[from] LibraryError),
302
303    #[error("{0}")]
304    ConflictingNames(String),
305}
306
307/// Calculates the relative path for a track.
308///
309/// # Errors
310///
311/// Returns an error if database references cannot be obtained
312pub fn calculate_rel_path(metadata: &TrackMeta) -> Result<PathBuf, TrackRenameError> {
313    let db = LibraryDb::open()?;
314    let artists = metadata.artists.artists(&db)?;
315    let album = metadata.album(&db)?;
316
317    let mut format_table = FormatTable::new();
318    format_table.extend_from_taggable(metadata);
319    add_from_artists(&mut format_table, &artists, "track");
320    if let Some(album) = album {
321        format_table.extend_from_taggable(&album);
322    }
323
324    let path = PathBuf::from(format_str(
325        &common_config().track_name.format_string,
326        &format_table,
327    )?);
328
329    Ok(path)
330}