1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use crate::prelude::*;
use claxon::Error as ClaxonError;
use claxon::FlacReader;
use claxon::metadata::StreamInfo;
use lofty::id3::v2::Id3v2Tag;
use lofty::tag::Tag;
use once_cell::sync::OnceCell;
/// A representation of a FLAC file.
pub struct FlacFile {
/// Path to the file
pub path: PathBuf,
/// File name without the extension.
pub file_name: String,
/// Subdirectory of the file.
pub sub_dir: PathBuf,
/// Cached raw Vorbis tags.
///
/// Lazily loaded. Uses thread-safe `OnceCell`.
vorbis_tags: OnceCell<Tag>,
/// Cached ID3 tags.
///
/// Lazily loaded. Uses thread-safe `OnceCell`.
id3_tags: OnceCell<Id3v2Tag>,
/// Disc context for track renaming.
///
/// Set once after collection
pub disc_context: Option<DiscContext>,
}
impl FlacFile {
/// Create a new [`FlacFile`] from a path.
#[must_use]
pub fn new(path: PathBuf, source_dir: &PathBuf) -> Self {
let sub_dir = path
.strip_prefix(source_dir)
.expect("Flac file path should start with the source directory")
.parent()
.expect("Flac file path should have a parent directory")
.to_path_buf();
let file_name = path
.file_stem()
.expect("Flac file should have a name")
.to_string_lossy()
.into_owned();
FlacFile {
path,
file_name,
sub_dir,
vorbis_tags: OnceCell::new(),
id3_tags: OnceCell::new(),
disc_context: None,
}
}
/// Get cached raw Vorbis tags without any conversion.
pub fn vorbis_tags(&self) -> Result<&Tag, Failure<TranscodeAction>> {
self.vorbis_tags.get_or_try_init(|| {
get_vorbis_tags(self).map_err(Failure::wrap(TranscodeAction::GetTags))
})
}
/// Get cached ID3 tags, round-tripped through [`Id3v2Tag`] conversion.
///
/// Values that cannot be represented in `ID3v2` format (e.g. non-numeric
/// track numbers) are dropped during the round-trip, matching the
/// behavior of [`save_id3v2_deterministic`].
pub fn id3_tags(&self) -> Result<&Id3v2Tag, Failure<TranscodeAction>> {
self.id3_tags.get_or_try_init(|| {
let mut tags = self.vorbis_tags()?.clone();
fix_track_numbering(&mut tags);
Ok(Id3v2Tag::from(tags))
})
}
/// Full path as a string.
#[must_use]
pub fn get_path_string(&self) -> String {
self.path.to_string_lossy().into_owned()
}
/// FLAC stream info containing sample rate, channels, and bit depth.
pub fn get_stream_info(&self) -> Result<StreamInfo, ClaxonError> {
let reader = FlacReader::open(&self.path)?;
Ok(reader.streaminfo())
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Lowercase `.flac` extension is stripped from the file name.
#[test]
fn flac_file_new_lowercase() {
// Arrange
let source_dir = PathBuf::from("/music");
let path = PathBuf::from("/music/01. Track.flac");
// Act
let flac = FlacFile::new(path, &source_dir);
// Assert
assert_eq!(flac.file_name, "01. Track");
}
/// Uppercase `.FLAC` extension is stripped from the file name.
#[test]
fn flac_file_new_uppercase() {
// Arrange
let source_dir = PathBuf::from("/music");
let path = PathBuf::from("/music/01. Track.FLAC");
// Act
let flac = FlacFile::new(path, &source_dir);
// Assert
assert_eq!(flac.file_name, "01. Track");
}
}