Skip to main content

selene_core/library/track/
core_impls.rs

1use std::path::PathBuf;
2
3use blake3::Hash;
4use chrono::{Datelike, Timelike};
5use lofty::{
6    ogg::VorbisComments,
7    tag::{Accessor, items::Timestamp},
8};
9use lunar_lib::{
10    database::{Database, DatabaseEntry, DatabaseError, TransactionError},
11    formatter::{FormatError, FormatTable, format_str},
12};
13use thiserror::Error;
14
15use crate::{
16    config::common::common_config,
17    database::LibraryDb,
18    errors::{LibraryError, MetadataError},
19    library::{
20        album::Album,
21        artist::{Artist, add_from_artists},
22        loudnorm::LoudnormAnalysis,
23        track::{
24            Track, TrackId,
25            track_meta::{TrackAlbumInfo, TrackMeta},
26        },
27    },
28    media_container::MediaContainer,
29};
30
31use super::lyric_data::LyricData;
32
33// Core
34impl Track {
35    #[must_use]
36    pub fn new(hash: Hash, container: MediaContainer, metadata: TrackMeta) -> Self {
37        Self {
38            id: TrackId::new(hash),
39            container,
40            metadata,
41            loudnorm_analysis: None,
42            version: Track::VERSION_NUMBER,
43        }
44    }
45}
46
47// Accessors
48impl Track {
49    #[must_use]
50    pub fn id(&self) -> TrackId {
51        self.id
52    }
53
54    #[must_use]
55    pub fn container(&self) -> &MediaContainer {
56        &self.container
57    }
58
59    #[must_use]
60    pub fn loudnorm_analysis(&self) -> Option<&LoudnormAnalysis> {
61        self.loudnorm_analysis.as_ref()
62    }
63
64    #[must_use]
65    pub fn is_single(&self) -> bool {
66        self.metadata.album.is_none()
67    }
68
69    pub fn album(&self, db: &LibraryDb) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
70        if let Some(album_id) = self.metadata.album {
71            Ok(Some({
72                let album = Album::db_get_from(album_id, db)?.expect("Dangling album reference");
73
74                let reference = album
75                    .track_refs()
76                    .iter()
77                    .find(|t| t.id == self.id)
78                    .expect("Track not found in album");
79
80                let track_num = reference.track_num;
81                let disc_num = reference.disc_num;
82
83                TrackAlbumInfo {
84                    album,
85                    track_num,
86                    disc_num,
87                }
88            }))
89        } else {
90            Ok(None)
91        }
92    }
93}
94
95// Mutators
96impl Track {
97    pub fn metadata_key_values(&self) -> Result<VorbisComments, MetadataError> {
98        let mut tags = VorbisComments::new();
99        {
100            let db = LibraryDb::open()?;
101            if let Some(track_album_info) = self.album(&db)? {
102                tags.set_album(track_album_info.album.name.clone());
103
104                let artists = track_album_info
105                    .album
106                    .artists()
107                    .artists(&db)?
108                    .iter()
109                    .map(Artist::name)
110                    .collect::<Vec<_>>()
111                    .join(";");
112                tags.insert("ALBUMARTIST".to_owned(), artists);
113
114                if let Some(track_num) = track_album_info.track_num {
115                    tags.set_track(track_num);
116                    if let Some(track_total) = track_album_info.album.track_total {
117                        tags.set_track_total(track_total);
118                    }
119                }
120
121                if let Some(disc_num) = track_album_info.disc_num {
122                    tags.set_disk(disc_num);
123                    if let Some(disc_total) = track_album_info.album.disc_total {
124                        tags.set_disk_total(disc_total);
125                    }
126                }
127            }
128
129            let artists = self
130                .metadata
131                .artists
132                .artists(&db)?
133                .iter()
134                .map(Artist::name)
135                .collect::<Vec<_>>()
136                .join(";");
137            tags.set_artist(artists);
138        }
139
140        if let Some(date) = self.metadata.date {
141            let ts = Timestamp {
142                year: date.year() as u16,
143                month: Some(date.month() as u8),
144                day: Some(date.day() as u8),
145                hour: Some(date.hour() as u8),
146                minute: Some(date.minute() as u8),
147                second: Some(date.second() as u8),
148            };
149            tags.set_date(ts);
150        }
151
152        for genre in &self.metadata.genre {
153            tags.push("GENRE".to_owned(), genre.to_owned());
154        }
155
156        if let Some(title) = &self.metadata.title {
157            tags.set_title(title.to_owned());
158        }
159
160        if let Some(lyric_data) = &self.metadata.lyric_data {
161            match lyric_data {
162                LyricData::Instrumental => {
163                    tags.insert("INSTRUMENTAL".to_owned(), "1".to_owned());
164                }
165                LyricData::Plain(lyrics) => {
166                    tags.insert("UNSYNCEDLYRICS".to_owned(), lyrics.to_string());
167                }
168                LyricData::Synced(lyrics) => {
169                    tags.insert("SYNCEDLYRICS".to_owned(), lyrics.to_lrc_string());
170                }
171            }
172        }
173
174        self.metadata.other.iter().for_each(|(k, v)| {
175            tags.insert(k.to_uppercase(), v.to_owned());
176        });
177
178        Ok(tags)
179    }
180}
181
182#[derive(Debug, Error)]
183pub enum TrackRenameError {
184    #[error("IoError: {0}")]
185    Io(#[from] std::io::Error),
186
187    #[error("DatabaseError: {0}")]
188    Database(#[from] DatabaseError),
189
190    #[error("TransactionError: {0}")]
191    Transaction(#[from] TransactionError),
192
193    #[error("FormatError: {0}")]
194    Format(#[from] FormatError),
195
196    #[error("LibraryError: {0}")]
197    Library(#[from] LibraryError),
198
199    #[error("{0}")]
200    ConflictingNames(String),
201}
202
203/// Calculates the relative path for a track.
204///
205/// # Errors
206///
207/// Returns an error if database references cannot be obtained
208pub fn calculate_rel_path(metadata: &TrackMeta) -> Result<PathBuf, TrackRenameError> {
209    let db = LibraryDb::open()?;
210    let artists = metadata.artists.artists(&db)?;
211    let album = metadata.album(&db)?;
212
213    let mut format_table = FormatTable::new();
214    format_table.extend_from_taggable(metadata);
215    add_from_artists(&mut format_table, &artists, "track");
216    if let Some(album) = album {
217        format_table.extend_from_taggable(&album);
218    }
219
220    let path = PathBuf::from(format_str(
221        &common_config().track_name.format_string,
222        &format_table,
223    )?);
224
225    Ok(path)
226}