selene_core/library/track/
core_impls.rs1use std::sync::Arc;
2
3use blake3::Hash;
4use lofty::ogg::VorbisComments;
5use lunar_lib::{
6 database::{DatabaseEntry, DatabaseError, TransactionError},
7 iterator_ext::IteratorExtensions,
8};
9
10use crate::{
11 config::{ExportConfig, MultiValueStrategy},
12 database::LibraryDb,
13 library::{
14 album::Album,
15 artist::Artist,
16 loudnorm::LoudnormAnalysis,
17 track::{
18 ResolvedTrack, Track, TrackId,
19 lyric_data::LyricData,
20 track_meta::{TrackAlbumInfo, TrackMeta},
21 },
22 },
23 lyrics::LyricFormat,
24 media_container::MediaContainer,
25};
26
27impl Track {
29 #[must_use]
30 pub fn new(hash: Hash, container: MediaContainer, metadata: TrackMeta) -> Self {
31 Self {
32 id: TrackId::new(hash),
33 container,
34 metadata,
35 loudnorm_analysis: None,
36 }
37 }
38}
39
40impl Track {
42 #[must_use]
43 pub fn id(&self) -> TrackId {
44 self.id
45 }
46
47 #[must_use]
48 pub fn container(&self) -> &MediaContainer {
49 &self.container
50 }
51
52 #[must_use]
53 pub fn loudnorm_analysis(&self) -> Option<&LoudnormAnalysis> {
54 self.loudnorm_analysis.as_ref()
55 }
56
57 #[must_use]
58 pub fn is_single(&self) -> bool {
59 self.metadata.album.is_none()
60 }
61
62 #[must_use]
63 pub fn metadata(&self) -> &TrackMeta {
64 &self.metadata
65 }
66
67 #[must_use]
68 pub fn metadata_mut(&mut self) -> &mut TrackMeta {
69 &mut self.metadata
70 }
71
72 pub fn album(&self, db: &LibraryDb) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
73 if let Some(album_id) = self.metadata.album {
74 Ok(Some({
75 let album = Album::db_get(album_id, db)?.expect("Dangling album reference");
76
77 let reference = album
78 .track_refs()
79 .iter()
80 .find(|t| t.id == self.id)
81 .expect("Track not found in album");
82
83 let track_num = reference.track_num;
84 let disc_num = reference.disc_num;
85
86 TrackAlbumInfo {
87 album,
88 track_num,
89 disc_num,
90 }
91 }))
92 } else {
93 Ok(None)
94 }
95 }
96}
97
98impl ResolvedTrack {
99 pub fn metadata_key_values(
100 &self,
101 export_settings: &ExportConfig,
102 ) -> Result<VorbisComments, TransactionError> {
103 let mut tags = VorbisComments::new();
104
105 if let Some((al, ar, tn, dn)) = self.album_info() {
106 tags.insert(String::from("ALBUM"), al.name().to_owned());
107
108 apply_artists(
109 &mut tags,
110 ar,
111 &export_settings.multi_value_strategy,
112 "ALBUMARTIST",
113 );
114
115 if let Some(track_total) = al.track_total {
116 tags.insert(String::from("TRACKTOTAL"), track_total.to_string());
117 }
118 if let Some(disc_total) = al.disc_total {
119 tags.insert(String::from("DISCTOTAL"), disc_total.to_string());
120 }
121 if let Some(track_num) = tn {
122 tags.insert(String::from("TRACKNUMBER"), track_num.to_string());
123 }
124 if let Some(disc_num) = dn {
125 tags.insert(String::from("DISCNUMBER"), disc_num.to_string());
126 }
127 } else if export_settings.singles_as_albums {
128 tags.insert(
129 String::from("ALBUM"),
130 self.track.metadata().safe_title().to_owned(),
131 );
132
133 apply_artists(
134 &mut tags,
135 self.artists(),
136 &export_settings.multi_value_strategy,
137 "ALBUMARTIST",
138 );
139 tags.insert(String::from("TRACKTOTAL"), String::from("1"));
140 tags.insert(String::from("DISCTOTAL"), String::from("1"));
141 tags.insert(String::from("TRACKNUMBER"), String::from("1"));
142 tags.insert(String::from("DISCNUMBER"), String::from("1"));
143 }
144
145 apply_artists(
146 &mut tags,
147 &self.artists,
148 &export_settings.multi_value_strategy,
149 "ARTIST",
150 );
151
152 if let Some(date) = self.metadata.date {
153 tags.insert(
154 String::from("DATE"),
155 date.format("%Y-%m-%dT%H:%M:%S").to_string(),
156 );
157 }
158
159 for genre in &self.metadata.genre {
160 tags.push(String::from("GENRE"), genre.to_owned());
161 }
162
163 if let Some(title) = &self.metadata.title {
164 tags.insert(String::from("TITLE"), title.to_owned())
165 }
166
167 if let Some(lyric_data) = &self.metadata.lyric_data {
168 match lyric_data {
169 LyricData::Instrumental => {
170 tags.insert(String::from("INSTRUMENTAL"), "1".to_owned());
171 }
172 LyricData::Plain(lyrics) => {
173 tags.insert(String::from("UNSYNCEDLYRICS"), lyrics.to_string());
174 }
175 LyricData::Synced(lyrics) => {
176 tags.insert(
177 String::from("SYNCEDLYRICS"),
178 lyrics.to_lyrics(LyricFormat::Lrc { a2: false }),
179 );
180 }
181 }
182 }
183
184 if let Some(loudnorm_analysis) = self.loudnorm_analysis() {
185 tags.insert(
186 String::from("REPLAYGAIN_TRACK_GAIN"),
187 format!("{} dB", loudnorm_analysis.calculated_gain_db()),
188 );
189 tags.insert(
190 String::from("REPLAYGAIN_TRACK_PEAK"),
191 format!("{}", loudnorm_analysis.calculated_replay_gain_peak()),
192 );
193 }
194
195 self.metadata.other.iter().for_each(|(k, v)| {
196 tags.push(k.to_uppercase(), v.to_owned());
197 });
198
199 Ok(tags)
200 }
201}
202
203fn apply_artists(
204 tags: &mut VorbisComments,
205 artists: &[Arc<Artist>],
206 strategy: &MultiValueStrategy,
207 tag: &'static str,
208) {
209 match strategy {
210 MultiValueStrategy::MultipleTags => {
211 for artist in artists {
212 tags.push(tag.to_owned(), artist.name().to_owned());
213 }
214 }
215 MultiValueStrategy::JoinBy(sep) => {
216 tags.insert(
217 tag.to_owned(),
218 artists.iter().map(|a| a.name()).to_vec().join(&sep),
219 );
220 }
221 MultiValueStrategy::First => {
222 if let Some(artist) = artists.first() {
223 tags.insert(tag.to_owned(), artist.name().to_owned());
224 }
225 }
226 }
227}