selene_core/library/track/
core_impls.rs1use 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
32impl 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
54impl 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
130impl 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 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
329pub 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}