selene_core/library/track/
core_impls.rs1use 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
37impl 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
59impl 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
135impl 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 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
307pub 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}