Skip to main content

musefs_db/
models.rs

1use strum::{EnumIter, EnumString, IntoStaticStr};
2
3/// The DB text representation (the `tracks.format` column) is derived:
4/// `serialize_all = "lowercase"` lowercases the whole variant ident
5/// (`OggFlac` → `"oggflac"`). The strings are an external contract —
6/// beets/Picard write them — pinned by `tests::db_strings_are_pinned`.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, IntoStaticStr, EnumIter)]
8#[strum(serialize_all = "lowercase")]
9#[cfg_attr(feature = "mutants", derive(Default))]
10pub enum Format {
11    #[cfg_attr(feature = "mutants", default)]
12    Flac,
13    Mp3,
14    M4a,
15    Opus,
16    Vorbis,
17    OggFlac,
18    Wav,
19}
20
21impl Format {
22    pub fn as_str(self) -> &'static str {
23        self.into()
24    }
25}
26
27#[cfg(test)]
28mod tests {
29    use super::Format;
30    use strum::IntoEnumIterator;
31
32    #[test]
33    fn every_format_round_trips() {
34        for f in Format::iter() {
35            assert_eq!(f.as_str().parse::<Format>(), Ok(f));
36        }
37    }
38
39    /// The strings are a DB contract — external writers (beets/Picard) store
40    /// them. A variant rename must not silently change the stored string.
41    #[test]
42    fn db_strings_are_pinned() {
43        let expected = [
44            (Format::Flac, "flac"),
45            (Format::Mp3, "mp3"),
46            (Format::M4a, "m4a"),
47            (Format::Opus, "opus"),
48            (Format::Vorbis, "vorbis"),
49            (Format::OggFlac, "oggflac"),
50            (Format::Wav, "wav"),
51        ];
52        assert_eq!(expected.len(), Format::iter().count());
53        for (f, s) in expected {
54            assert_eq!(f.as_str(), s);
55        }
56    }
57}
58
59#[cfg(test)]
60mod binary_tag_models_tests {
61    #[test]
62    fn binary_tag_constructs() {
63        let bt = super::BinaryTag {
64            key: "PRIV".to_string(),
65            payload: vec![1, 2, 3],
66            ordinal: 0,
67        };
68        assert_eq!(bt.payload.len(), 3);
69        let row = super::BinaryTagRow {
70            rowid: 7,
71            key: "PRIV".to_string(),
72            byte_len: 3,
73        };
74        assert_eq!(row.rowid, 7);
75        let sb = super::StructuralBlock {
76            kind: "STREAMINFO".to_string(),
77            ordinal: 0,
78            body: vec![0u8; 34],
79        };
80        assert_eq!(sb.body.len(), 34);
81    }
82}
83
84/// Validated audio-region bounds for a track: `audio_offset + audio_length`
85/// is guaranteed to fit within `backing_size`, so the reader can splice the
86/// audio region without re-checking. Built at the `tracks` row reader.
87#[cfg_attr(feature = "mutants", derive(Default))]
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct TrackBounds {
90    audio_offset: u64,
91    audio_length: u64,
92}
93
94impl TrackBounds {
95    /// Err if `audio_offset + audio_length` overflows or exceeds `backing_size`.
96    pub fn new(
97        audio_offset: u64,
98        audio_length: u64,
99        backing_size: u64,
100    ) -> Result<TrackBounds, crate::DbError> {
101        let end = audio_offset
102            .checked_add(audio_length)
103            .filter(|&end| end <= backing_size)
104            .ok_or(crate::DbError::AudioBoundsOutOfRange {
105                audio_offset,
106                audio_length,
107                backing_size,
108            })?;
109        let _ = end;
110        Ok(TrackBounds {
111            audio_offset,
112            audio_length,
113        })
114    }
115
116    pub fn audio_offset(&self) -> u64 {
117        self.audio_offset
118    }
119
120    pub fn audio_length(&self) -> u64 {
121        self.audio_length
122    }
123}
124
125#[cfg_attr(feature = "mutants", derive(Default))]
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Track {
128    pub id: i64,
129    pub backing_path: String,
130    pub format: Format,
131    pub bounds: TrackBounds,
132    pub backing_size: u64,
133    pub backing_mtime_ns: i64,
134    pub backing_ctime_ns: i64,
135    pub content_version: i64,
136    pub updated_at: i64,
137    pub fingerprint: Option<String>,
138    pub content_hash: Option<String>,
139}
140
141#[derive(Debug, Clone)]
142pub struct NewTrack {
143    pub backing_path: String,
144    pub format: Format,
145    pub audio_offset: u64,
146    pub audio_length: u64,
147    pub backing_size: u64,
148    pub backing_mtime_ns: i64,
149    pub backing_ctime_ns: i64,
150}
151
152#[derive(Debug, Clone)]
153pub struct NewArt {
154    pub mime: String,
155    pub width: Option<u32>,
156    pub height: Option<u32>,
157    pub data: Vec<u8>,
158}
159
160#[cfg_attr(feature = "mutants", derive(Default))]
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct Tag {
163    pub key: String,
164    pub value: String,
165    pub ordinal: u64,
166}
167
168impl Tag {
169    pub fn new(key: &str, value: &str, ordinal: u64) -> Tag {
170        Tag {
171            key: key.to_string(),
172            value: value.to_string(),
173            ordinal,
174        }
175    }
176}
177
178#[cfg_attr(feature = "mutants", derive(Default))]
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct Art {
181    pub id: i64,
182    pub sha256: String,
183    pub mime: String,
184    pub width: Option<u32>,
185    pub height: Option<u32>,
186    pub byte_len: u64,
187    pub data: Vec<u8>,
188}
189
190#[cfg_attr(feature = "mutants", derive(Default))]
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct ArtMeta {
193    pub mime: String,
194    pub width: Option<u32>,
195    pub height: Option<u32>,
196    pub byte_len: u64,
197}
198
199#[cfg_attr(feature = "mutants", derive(Default))]
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct TrackArt {
202    pub art_id: i64,
203    pub picture_type: u32,
204    pub description: String,
205    pub ordinal: u64,
206}
207
208/// A binary tag payload to write (e.g. an opaque ID3 `PRIV` frame body). `key` is
209/// the format-private identifier (ID3 frame id, `APPLICATION`/`CUESHEET`,
210/// `----:<mean>:<name>`); `payload` is the post-header frame/block body.
211#[cfg_attr(feature = "mutants", derive(Default))]
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct BinaryTag {
214    pub key: String,
215    pub payload: Vec<u8>,
216    pub ordinal: u64,
217}
218
219/// A binary tag row read back for synthesis: the streaming handle (`rowid`), the
220/// key, and the payload length — the bytes themselves stream at read time.
221#[cfg_attr(feature = "mutants", derive(Default))]
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct BinaryTagRow {
224    pub rowid: i64,
225    pub key: String,
226    pub byte_len: u64,
227}
228
229/// A read-only structural metadata block derived from the backing file
230/// (FLAC `STREAMINFO`/`SEEKTABLE`). Stored outside the editable `tags` contract.
231#[cfg_attr(feature = "mutants", derive(Default))]
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct StructuralBlock {
234    pub kind: String,
235    pub ordinal: u64,
236    pub body: Vec<u8>,
237}
238
239#[cfg(test)]
240mod track_bounds_tests {
241    use super::TrackBounds;
242
243    #[test]
244    fn accepts_in_range() {
245        let b = TrackBounds::new(10, 20, 100).unwrap();
246        assert_eq!(b.audio_offset(), 10);
247        assert_eq!(b.audio_length(), 20);
248    }
249
250    #[test]
251    fn accepts_exact_fit() {
252        let b = TrackBounds::new(30, 70, 100).unwrap();
253        assert_eq!(b.audio_offset(), 30);
254        assert_eq!(b.audio_length(), 70);
255    }
256
257    #[test]
258    fn accepts_zero_length() {
259        // A zero-length audio run is valid (e.g. structure-only edge).
260        let b = TrackBounds::new(0, 0, 0).unwrap();
261        assert_eq!(b.audio_length(), 0);
262    }
263
264    #[test]
265    fn rejects_exceeding_backing_size() {
266        assert!(TrackBounds::new(50, 60, 100).is_err());
267    }
268
269    #[test]
270    fn rejects_offset_plus_length_overflow() {
271        assert!(TrackBounds::new(u64::MAX, 1, u64::MAX).is_err());
272    }
273}