selene_core/library/track/
core_impls.rs1use 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
33impl 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
47impl 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
95impl 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
203pub 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}