1use crate::library_db::const_unknown::{UNKNOWN_ARTIST, UNKNOWN_TITLE};
2use crate::podcast::episode::Episode;
3use crate::songtag::lrc::Lyric;
27use crate::utils::get_parent_folder;
28use anyhow::{bail, Context, Result};
29use id3::frame::Lyrics as Id3Lyrics;
30use lofty::config::WriteOptions;
31use lofty::id3::v2::{Frame, Id3v2Tag, UnsynchronizedTextFrame};
32use lofty::picture::{Picture, PictureType};
33use lofty::prelude::{Accessor, AudioFile, ItemKey, TagExt, TaggedFileExt};
34use lofty::tag::{ItemValue, Tag as LoftyTag, TagItem};
35use lofty::{file::FileType, probe::Probe};
36use std::convert::From;
37use std::ffi::OsStr;
38use std::fs::rename;
39use std::path::{Path, PathBuf};
40use std::str::FromStr;
41use std::time::{Duration, SystemTime};
42
43#[derive(Clone, Debug, PartialEq)]
45pub enum LocationType {
46 Path(PathBuf),
48 Uri(String),
50}
51
52impl From<PathBuf> for LocationType {
53 fn from(value: PathBuf) -> Self {
54 Self::Path(value)
55 }
56}
57
58#[derive(Clone, Debug)]
59pub struct Track {
60 location: LocationType,
62 pub media_type: MediaType,
63
64 artist: Option<String>,
66 album: Option<String>,
68 title: Option<String>,
70 duration: Duration,
72 pub last_modified: SystemTime,
73 lyric_frames: Vec<Id3Lyrics>,
75 lyric_selected_index: usize,
76 parsed_lyric: Option<Lyric>,
77 picture: Option<Picture>,
78 album_photo: Option<String>,
79 file_type: Option<FileType>,
80 genre: Option<String>,
83 pub podcast_localfile: Option<String>,
88}
89
90impl PartialEq for Track {
91 fn eq(&self, other: &Self) -> bool {
92 self.location == other.location
93 }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum MediaType {
98 Music,
99 Podcast,
100 LiveRadio,
101}
102
103impl Track {
104 #[allow(clippy::cast_sign_loss)]
106 #[must_use]
107 pub fn from_episode(ep: &Episode) -> Self {
108 let lyric_frames: Vec<Id3Lyrics> = Vec::new();
109 let mut podcast_localfile: Option<String> = None;
110 if let Some(path) = &ep.path {
111 if path.exists() {
112 podcast_localfile = Some(path.to_string_lossy().to_string());
113 }
114 }
115
116 Self {
117 artist: Some("Episode".to_string()),
118 album: None,
119 title: Some(ep.title.clone()),
120 location: LocationType::Uri(ep.url.clone()),
121 duration: Duration::from_secs(ep.duration.unwrap_or(0) as u64),
122 last_modified: SystemTime::now(),
123 lyric_frames,
124 lyric_selected_index: 0,
125 parsed_lyric: None,
126 picture: None,
127 album_photo: ep.image_url.clone(),
128 file_type: None,
129 genre: None,
130 media_type: MediaType::Podcast,
131 podcast_localfile,
132 }
133 }
134
135 pub fn read_from_path<P: AsRef<Path>>(path: P, for_db: bool) -> Result<Self> {
137 let path = path.as_ref();
138
139 let probe = Probe::open(path)?;
140
141 let mut song = Self::new(LocationType::Path(path.to_path_buf()), MediaType::Music);
142 let tagged_file = match probe.read() {
143 Ok(v) => Some(v),
144 Err(err) => {
145 warn!(
146 "Failed to read metadata from \"{}\": {}",
147 path.display(),
148 err
149 );
150 None
151 }
152 };
153
154 if let Some(mut tagged_file) = tagged_file {
155 let properties = tagged_file.properties();
157 song.duration = properties.duration();
158 song.file_type = Some(tagged_file.file_type());
159
160 if let Some(tag) = tagged_file.primary_tag_mut() {
161 Self::process_tag(tag, &mut song, for_db)?;
162 } else if let Some(tag) = tagged_file.first_tag_mut() {
163 Self::process_tag(tag, &mut song, for_db)?;
164 } else {
165 warn!("File \"{}\" does not have any tags!", path.display());
166 }
167 }
168
169 if for_db {
171 return Ok(song);
172 }
173
174 let parent_folder = get_parent_folder(path);
175
176 if let Ok(files) = std::fs::read_dir(parent_folder) {
177 for f in files.flatten() {
178 let path = f.path();
179 if let Some(extension) = path.extension() {
180 if extension == "jpg" || extension == "png" {
181 song.album_photo = Some(path.to_string_lossy().to_string());
182 }
183 }
184 }
185 }
186
187 Ok(song)
188 }
189
190 fn process_tag(tag: &mut LoftyTag, track: &mut Track, for_db: bool) -> Result<()> {
192 if let Some(len_tag) = tag.get_string(&ItemKey::Length) {
194 track.duration = Duration::from_millis(len_tag.parse::<u64>()?);
195 }
196
197 track.artist = tag.artist().map(std::borrow::Cow::into_owned);
198 track.album = tag.album().map(std::borrow::Cow::into_owned);
199 track.title = tag.title().map(std::borrow::Cow::into_owned);
200 track.genre = tag.genre().map(std::borrow::Cow::into_owned);
201 track.media_type = MediaType::Music;
202
203 if for_db {
204 return Ok(());
205 }
206
207 let mut lyric_frames: Vec<Id3Lyrics> = Vec::new();
209 create_lyrics(tag, &mut lyric_frames);
210
211 track.parsed_lyric = lyric_frames
212 .first()
213 .and_then(|lf| Lyric::from_str(&lf.text).ok());
214 track.lyric_frames = lyric_frames;
215
216 let picture = tag
218 .pictures()
219 .iter()
220 .find(|pic| pic.pic_type() == PictureType::CoverFront)
221 .or_else(|| tag.pictures().first())
222 .cloned();
223
224 track.picture = picture;
225
226 Ok(())
227 }
228
229 #[must_use]
231 pub fn new_radio(url: &str) -> Self {
232 let mut track = Self::new(LocationType::Uri(url.to_string()), MediaType::LiveRadio);
233 track.artist = Some("Radio".to_string());
234 track.title = Some("Radio Station".to_string());
235 track.album = Some("Live".to_string());
236 track
237 }
238
239 #[must_use]
240 fn new(location: LocationType, media_type: MediaType) -> Self {
241 let duration = Duration::from_secs(0);
242 let lyric_frames: Vec<Id3Lyrics> = Vec::new();
243 let mut last_modified = SystemTime::now();
244 let mut title = None;
245
246 if let LocationType::Path(path) = &location {
247 if let Ok(meta) = path.metadata() {
248 if let Ok(modified) = meta.modified() {
249 last_modified = modified;
250 }
251 }
252
253 title = path.file_stem().and_then(OsStr::to_str).map(String::from);
254 }
255
256 Self {
257 file_type: None,
258 artist: None,
259 album: None,
260 title,
261 duration,
262 location,
263 parsed_lyric: None,
264 lyric_frames,
265 lyric_selected_index: 0,
266 picture: None,
267 album_photo: None,
268 last_modified,
269 genre: None,
270 media_type,
271 podcast_localfile: None,
272 }
273 }
274
275 pub fn adjust_lyric_delay(&mut self, time_pos: Duration, offset: i64) -> Result<()> {
276 if let Some(lyric) = self.parsed_lyric.as_mut() {
277 lyric.adjust_offset(time_pos, offset);
278 let text = lyric.as_lrc_text();
279 self.set_lyric(&text, "Adjusted");
280 self.save_tag()?;
281 }
282 Ok(())
283 }
284
285 pub fn cycle_lyrics(&mut self) -> Result<&Id3Lyrics> {
286 if self.lyric_frames_is_empty() {
287 bail!("no lyrics embedded");
288 }
289
290 self.lyric_selected_index += 1;
291 if self.lyric_selected_index >= self.lyric_frames.len() {
292 self.lyric_selected_index = 0;
293 }
294
295 if let Some(f) = self.lyric_frames.get(self.lyric_selected_index) {
296 if let Ok(parsed_lyric) = Lyric::from_str(&f.text) {
297 self.parsed_lyric = Some(parsed_lyric);
298 return Ok(f);
299 }
300 }
301
302 bail!("cycle lyrics error")
303 }
304
305 #[must_use]
306 pub const fn parsed_lyric(&self) -> Option<&Lyric> {
307 self.parsed_lyric.as_ref()
308 }
309
310 pub fn set_parsed_lyric(&mut self, pl: Option<Lyric>) {
311 self.parsed_lyric = pl;
312 }
313
314 pub fn lyric_frames_remove_selected(&mut self) {
315 self.lyric_frames.remove(self.lyric_selected_index);
316 }
317
318 pub fn set_lyric_selected_index(&mut self, index: usize) {
319 self.lyric_selected_index = index;
320 }
321
322 #[must_use]
323 pub const fn lyric_selected_index(&self) -> usize {
324 self.lyric_selected_index
325 }
326
327 #[must_use]
328 pub fn lyric_selected(&self) -> Option<&Id3Lyrics> {
329 if self.lyric_frames.is_empty() {
330 return None;
331 }
332 if let Some(lf) = self.lyric_frames.get(self.lyric_selected_index) {
333 return Some(lf);
334 }
335 None
336 }
337
338 #[must_use]
339 pub fn lyric_frames_is_empty(&self) -> bool {
340 self.lyric_frames.is_empty()
341 }
342
343 #[must_use]
344 pub fn lyric_frames_len(&self) -> usize {
345 if self.lyric_frames.is_empty() {
346 return 0;
347 }
348 self.lyric_frames.len()
349 }
350
351 #[must_use]
352 pub fn lyric_frames(&self) -> Option<Vec<Id3Lyrics>> {
353 if self.lyric_frames.is_empty() {
354 return None;
355 }
356 Some(self.lyric_frames.clone())
357 }
358
359 #[must_use]
360 pub const fn picture(&self) -> Option<&Picture> {
361 self.picture.as_ref()
362 }
363 #[must_use]
364 pub fn album_photo(&self) -> Option<&str> {
365 self.album_photo.as_deref()
366 }
367
368 #[must_use]
371 pub fn artist(&self) -> Option<&str> {
372 self.artist.as_deref()
373 }
374
375 pub fn set_artist(&mut self, a: &str) {
376 self.artist = Some(a.to_string());
377 }
378
379 #[must_use]
382 pub fn album(&self) -> Option<&str> {
383 self.album.as_deref()
384 }
385
386 pub fn set_album(&mut self, album: &str) {
387 self.album = Some(album.to_string());
388 }
389
390 #[must_use]
391 pub fn genre(&self) -> Option<&str> {
392 self.genre.as_deref()
393 }
394
395 #[allow(unused)]
396 pub fn set_genre(&mut self, genre: &str) {
397 self.genre = Some(genre.to_string());
398 }
399
400 #[must_use]
403 pub fn title(&self) -> Option<&str> {
404 self.title.as_deref()
405 }
406
407 pub fn set_title(&mut self, title: &str) {
408 self.title = Some(title.to_string());
409 }
410
411 #[must_use]
413 pub fn file(&self) -> Option<&str> {
414 match &self.location {
415 LocationType::Path(path_buf) => path_buf.to_str(),
416 LocationType::Uri(uri) => Some(uri),
417 }
418 }
419
420 pub fn directory(&self) -> Option<&str> {
422 if let LocationType::Path(path) = &self.location {
423 path.parent().and_then(Path::to_str)
425 } else {
426 None
427 }
428 }
429
430 pub fn ext(&self) -> Option<&str> {
432 if let LocationType::Path(path) = &self.location {
433 path.extension().and_then(OsStr::to_str)
434 } else {
435 None
436 }
437 }
438
439 #[must_use]
440 pub const fn duration(&self) -> Duration {
441 self.duration
442 }
443
444 #[must_use]
445 pub fn duration_formatted(&self) -> String {
446 Self::duration_formatted_short(&self.duration)
447 }
448
449 #[must_use]
450 pub fn duration_formatted_short(d: &Duration) -> String {
451 let duration_hour = d.as_secs() / 3600;
452 let duration_min = (d.as_secs() % 3600) / 60;
453 let duration_secs = d.as_secs() % 60;
454
455 if duration_hour == 0 {
456 format!("{duration_min:0>2}:{duration_secs:0>2}")
457 } else {
458 format!("{duration_hour}:{duration_min:0>2}:{duration_secs:0>2}")
459 }
460 }
461
462 pub fn name(&self) -> Option<&str> {
464 match &self.location {
465 LocationType::Path(path) => path.file_name().and_then(OsStr::to_str),
466 LocationType::Uri(uri) => Some(uri),
468 }
469 }
470
471 pub fn save_tag(&mut self) -> Result<()> {
472 match self.file_type {
473 Some(FileType::Mpeg) => {
474 if let Some(file_path) = self.file() {
475 let mut tag = Id3v2Tag::default();
476 self.update_tag(&mut tag);
477
478 if !self.lyric_frames_is_empty() {
479 if let Some(lyric_frames) = self.lyric_frames() {
480 for l in lyric_frames {
481 let l_frame =
482 Frame::UnsynchronizedText(UnsynchronizedTextFrame::new(
483 lofty::TextEncoding::UTF8,
484 l.lang.as_bytes()[0..3]
485 .try_into()
486 .with_context(|| "wrong length of language")?,
487 l.description,
488 l.text,
489 ));
490
491 tag.insert(l_frame);
492 }
493 }
494 }
495
496 if let Some(any_picture) = self.picture().cloned() {
497 tag.insert_picture(any_picture);
498 }
499
500 tag.save_to_path(file_path, WriteOptions::new())?;
501 }
502 }
503 _ => {
504 if let Some(file_path) = self.file() {
505 let tag_type = match self.file_type {
506 Some(file_type) => file_type.primary_tag_type(),
507 None => return Ok(()),
508 };
509
510 let mut tag = LoftyTag::new(tag_type);
511 self.update_tag(&mut tag);
512
513 if !self.lyric_frames_is_empty() {
514 if let Some(lyric_frames) = self.lyric_frames() {
515 for l in lyric_frames {
516 tag.push(TagItem::new(ItemKey::Lyrics, ItemValue::Text(l.text)));
517 }
518 }
519 }
520
521 if let Some(any_picture) = self.picture().cloned() {
522 tag.push_picture(any_picture);
523 }
524
525 tag.save_to_path(file_path, WriteOptions::new())?;
526 }
527 }
528 }
529
530 self.rename_by_tag()?;
531 Ok(())
532 }
533
534 fn rename_by_tag(&mut self) -> Result<()> {
535 if let Some(ext) = self.ext() {
536 let new_name = format!(
537 "{}-{}.{}",
538 self.artist().unwrap_or(UNKNOWN_ARTIST),
539 self.title().unwrap_or(UNKNOWN_TITLE),
540 ext,
541 );
542
543 let new_name_path: &Path = Path::new(new_name.as_str());
544 if let Some(file) = self.file() {
545 let p_old: &Path = Path::new(file);
546 if let Some(p_prefix) = p_old.parent() {
547 let p_new = p_prefix.join(new_name_path);
548 rename(p_old, &p_new)?;
549 self.location = LocationType::Path(p_new);
550 }
551 }
552 }
553
554 Ok(())
555 }
556
557 pub fn set_lyric(&mut self, lyric_str: &str, lang_ext: &str) {
558 let mut lyric_frames = self.lyric_frames.clone();
559 match self.lyric_frames.get(self.lyric_selected_index) {
560 Some(lyric_frame) => {
561 lyric_frames[self.lyric_selected_index] = Id3Lyrics {
563 text: lyric_str.to_string(),
564 ..lyric_frame.clone()
565 };
566 }
567 None => {
568 lyric_frames.push(Id3Lyrics {
569 lang: "eng".to_string(),
570 description: lang_ext.to_string(),
571 text: lyric_str.to_string(),
572 });
573 }
574 }
575 self.lyric_frames = lyric_frames;
576 }
577
578 pub fn set_photo(&mut self, picture: Picture) {
579 self.picture = Some(picture);
580 }
581
582 fn update_tag<T: Accessor>(&self, tag: &mut T) {
583 tag.set_artist(
584 self.artist()
585 .map_or_else(|| String::from(UNKNOWN_ARTIST), str::to_string),
586 );
587
588 tag.set_title(
589 self.title()
590 .map_or_else(|| String::from(UNKNOWN_TITLE), str::to_string),
591 );
592
593 tag.set_album(self.album().map_or_else(String::new, str::to_string));
594 tag.set_genre(self.genre().map_or_else(String::new, str::to_string));
595 }
596}
597
598fn create_lyrics(tag: &mut LoftyTag, lyric_frames: &mut Vec<Id3Lyrics>) {
599 let lyrics = tag.take(&ItemKey::Lyrics);
600 for lyric in lyrics {
601 if let ItemValue::Text(lyrics_text) = lyric.value() {
602 lyric_frames.push(Id3Lyrics {
603 lang: lyric.lang().escape_ascii().to_string(),
604 description: lyric.description().to_string(),
605 text: lyrics_text.to_string(),
606 });
607 lyric_frames.sort_by(|a, b| {
608 a.description
609 .to_lowercase()
610 .cmp(&b.description.to_lowercase())
611 });
612 }
613 }
614}