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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! Provides the [`TagContainer` struct](TagContainer), a simple interface to access data in
//! Serato's tags.
use super::{
    beatgrid, color::Color, format::flac::FLACTag, format::id3::ID3Tag, format::mp4::MP4Tag,
    format::ogg::OggTag, generic, markers, Autotags, Beatgrid, Markers, Markers2, Overview,
};
use crate::error::Error;
use std::io;

/// Provides a streamlined interface for retrieving Serato tag data.
///
/// If you're not interested in the low-level format details, just use this instead of the
/// low-level structs (e.g. [`Markers`](Markers), [`Markers2`](Markers2), etc.)
///
/// Some of the data in Serato's tags is redundant and may contradict each other. This class
/// implements the same merge strategies for inconsistent data that Serato uses, too.
#[derive(Debug, Clone)]
pub struct TagContainer {
    autotags: Option<Autotags>,
    beatgrid: Option<Beatgrid>,
    markers: Option<Markers>,
    markers2: Option<Markers2>,
    overview: Option<Overview>,
}

/// The tag type of the data.
///
/// The format of the Serato tag data differs between tag types.
/// Therefore it's necessary to tell the parser from what kind of the the data originates from.
#[derive(Debug, Clone, Copy)]
pub enum TagFormat {
    ID3,
    FLAC,
    MP4,
    Ogg,
}

impl TagContainer {
    /// Create an empty Serato tag container.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            autotags: None,
            beatgrid: None,
            markers: None,
            markers2: None,
            overview: None,
        }
    }

    /// Parse the [`Serato Autotags`](Autotags) tag.
    pub fn parse_autotags(&mut self, input: &[u8], tag_format: TagFormat) -> Result<(), Error> {
        match tag_format {
            TagFormat::ID3 => {
                self.autotags = Some(Autotags::parse_id3(input)?);
            }
            TagFormat::FLAC => {
                self.autotags = Some(Autotags::parse_flac(input)?);
            }
            TagFormat::MP4 => {
                self.autotags = Some(Autotags::parse_mp4(input)?);
            }
            _ => return Err(Error::UnsupportedTagFormat),
        }
        Ok(())
    }

    /// Write the [`Serato Autotags`](Autotags) tag.
    pub fn write_autotags(
        &self,
        writer: &mut impl io::Write,
        tag_format: TagFormat,
    ) -> Result<usize, Error> {
        let tag = match &self.autotags {
            Some(x) => x,
            None => return Err(Error::NoTagDataAvailable),
        };
        match tag_format {
            TagFormat::ID3 => tag.write_id3(writer),
            TagFormat::FLAC => tag.write_flac(writer),
            TagFormat::MP4 => tag.write_mp4(writer),
            _ => Err(Error::UnsupportedTagFormat),
        }
    }

    /// Parse the [`Serato BeatGrid`](Beatgrid) tag.
    pub fn parse_beatgrid(&mut self, input: &[u8], tag_format: TagFormat) -> Result<(), Error> {
        match tag_format {
            TagFormat::ID3 => {
                self.beatgrid = Some(Beatgrid::parse_id3(input)?);
            }
            TagFormat::FLAC => {
                self.beatgrid = Some(Beatgrid::parse_flac(input)?);
            }
            TagFormat::MP4 => {
                self.beatgrid = Some(Beatgrid::parse_mp4(input)?);
            }
            _ => return Err(Error::UnsupportedTagFormat),
        }
        Ok(())
    }

    /// Write the [`Serato BeatGrid`](Beatgrid) tag.
    pub fn write_beatgrid(
        &self,
        writer: &mut impl io::Write,
        tag_format: TagFormat,
    ) -> Result<usize, Error> {
        let tag = match &self.beatgrid {
            Some(x) => x,
            None => return Err(Error::NoTagDataAvailable),
        };
        match tag_format {
            TagFormat::ID3 => tag.write_id3(writer),
            TagFormat::FLAC => tag.write_flac(writer),
            TagFormat::MP4 => tag.write_mp4(writer),
            _ => Err(Error::UnsupportedTagFormat),
        }
    }

    /// Parse the [`Serato Markers_`](Markers) tag.
    pub fn parse_markers(&mut self, input: &[u8], tag_format: TagFormat) -> Result<(), Error> {
        match tag_format {
            TagFormat::ID3 => {
                self.markers = Some(Markers::parse_id3(input)?);
            }
            TagFormat::MP4 => {
                self.markers = Some(Markers::parse_mp4(input)?);
            }
            _ => return Err(Error::UnsupportedTagFormat),
        }
        Ok(())
    }

    /// Write the [`Serato Markers_`](Markers) tag.
    pub fn write_markers(
        &self,
        writer: &mut impl io::Write,
        tag_format: TagFormat,
    ) -> Result<usize, Error> {
        let tag = match &self.markers {
            Some(x) => x,
            None => return Err(Error::NoTagDataAvailable),
        };
        match tag_format {
            TagFormat::ID3 => tag.write_id3(writer),
            TagFormat::MP4 => tag.write_mp4(writer),
            _ => Err(Error::UnsupportedTagFormat),
        }
    }

    /// Parse the [`Serato Markers2`](Markers2) tag.
    pub fn parse_markers2(&mut self, input: &[u8], tag_format: TagFormat) -> Result<(), Error> {
        match tag_format {
            TagFormat::ID3 => {
                self.markers2 = Some(Markers2::parse_id3(input)?);
            }
            TagFormat::FLAC => {
                self.markers2 = Some(Markers2::parse_flac(input)?);
            }
            TagFormat::MP4 => {
                self.markers2 = Some(Markers2::parse_mp4(input)?);
            }
            TagFormat::Ogg => {
                self.markers2 = Some(Markers2::parse_ogg(input)?);
            }
        }
        Ok(())
    }

    /// Write the [`Serato Markers2`](Markers2) tag.
    pub fn write_markers2(
        &self,
        writer: &mut impl io::Write,
        tag_format: TagFormat,
    ) -> Result<usize, Error> {
        let tag = match &self.markers2 {
            Some(x) => x,
            None => return Err(Error::NoTagDataAvailable),
        };
        match tag_format {
            TagFormat::ID3 => tag.write_id3(writer),
            TagFormat::FLAC => tag.write_flac(writer),
            TagFormat::MP4 => tag.write_mp4(writer),
            _ => Err(Error::UnsupportedTagFormat),
        }
    }

    /// Parse the [`Serato Overview`](Overview) tag.
    pub fn parse_overview(&mut self, input: &[u8], tag_format: TagFormat) -> Result<(), Error> {
        match tag_format {
            TagFormat::ID3 => {
                self.overview = Some(Overview::parse_id3(input)?);
            }
            TagFormat::FLAC => {
                self.overview = Some(Overview::parse_flac(input)?);
            }
            TagFormat::MP4 => {
                self.overview = Some(Overview::parse_mp4(input)?);
            }
            _ => return Err(Error::UnsupportedTagFormat),
        }
        Ok(())
    }

    /// Write the [`Serato Overview`](Overview) tag.
    pub fn write_overview(
        &self,
        writer: &mut impl io::Write,
        tag_format: TagFormat,
    ) -> Result<usize, Error> {
        let tag = match &self.overview {
            Some(x) => x,
            None => return Err(Error::NoTagDataAvailable),
        };
        match tag_format {
            TagFormat::ID3 => tag.write_id3(writer),
            TagFormat::FLAC => tag.write_flac(writer),
            TagFormat::MP4 => tag.write_mp4(writer),
            _ => Err(Error::UnsupportedTagFormat),
        }
    }

    /// Returns the [`auto_gain`](Autotags::auto_gain) value from the [`Serato Autotags`](Autotags) tag.
    #[must_use]
    pub fn auto_gain(&self) -> Option<f64> {
        if let Some(tag) = &self.autotags {
            return Some(tag.auto_gain);
        }

        None
    }

    /// Returns the [`gain_db`](Autotags::gain_db) value from the [`Serato Autotags`](Autotags) tag.
    #[must_use]
    pub fn gain_db(&self) -> Option<f64> {
        if let Some(tag) = &self.autotags {
            return Some(tag.gain_db);
        }

        None
    }

    /// Returns the beatgrid from the [`Serato BeatGrid`](Beatgrid) tag.
    #[must_use]
    pub fn beatgrid(
        &self,
    ) -> Option<(&Vec<beatgrid::NonTerminalMarker>, &beatgrid::TerminalMarker)> {
        if let Some(tag) = &self.beatgrid {
            return Some((&tag.non_terminal_markers, &tag.terminal_marker));
        }

        None
    }

    /// Returns BPM lock status from the [`Serato Markers2`](Markers2) tag.
    #[must_use]
    pub fn bpm_locked(&self) -> Option<bool> {
        if let Some(m) = &self.markers2 {
            return m.bpm_locked();
        }

        None
    }

    /// Returns cues from the [`Serato Markers_`](Markers) and [`Serato Markers2`](Markers2) tags.
    ///
    /// This retrieves the `Serato Markers2` cues first, then overwrite the values with those from
    /// `Serato Markers_`. This is what Serato does too (i.e. if `Serato Markers_` and `Serato
    /// Markers2` contradict each other, Serato will use the values from `Serato Markers_`).
    #[must_use]
    pub fn cues(&self) -> Vec<generic::Cue> {
        let mut map = std::collections::BTreeMap::new();

        // First, insert all cue from the `Serato Markers2` tag into the map.
        if let Some(m) = &self.markers2 {
            for cue in m.cues() {
                map.insert(cue.index, cue.to_owned());
            }
        }

        // Now, iterate over the cue markers from the `Serato Markers_` tag.
        if let Some(m) = &self.markers {
            for (index, marker) in m.cues() {
                match marker.marker_type {
                    // If a cue is set in `Serato Markers2` but is invalid in `Serato Markers_`,
                    // remove it.
                    markers::MarkerType::Invalid => {
                        map.remove(&index);
                        continue;
                    }
                    markers::MarkerType::Cue => {
                        if marker.start_position == None {
                            // This shouldn't be possible if the `Serato Markers_` data is valid.
                            // Ideally, this should be checked during the parsing state.
                            // FIXME: Throw error here?
                            map.remove(&index);
                            continue;
                        }

                        let position = marker.start_position.unwrap();

                        // If the cue is set in both `Serato Markers2` and `Serato Markers_`, use
                        // the version from `Serato Markers_`, but keep the label from `Serato
                        // Markers2` because the `Serato Markers_` tag doesn't contain labels.
                        let markers2_cue = map.remove(&index);
                        let label = match markers2_cue {
                            Some(c) => c.label,
                            None => String::new(),
                        };

                        map.insert(
                            index,
                            generic::Cue {
                                index,
                                position,
                                color: marker.color,
                                label,
                            },
                        );
                    }
                    _ => {} // Ignore loop markers
                }
            }
        }

        // Return the sorted list of cues.
        map.into_values().collect()
    }

    /// Returns loops from the [`Serato Markers_`](Markers) and [`Serato Markers2`](Markers2) tags.
    ///
    /// This retrieves the `Serato Markers2` loops first, then overwrite the values with those from
    /// `Serato Markers_`. This is what Serato does too (i.e. if `Serato Markers_` and `Serato
    /// Markers2` contradict each other, Serato will use the values from `Serato Markers_`).
    #[must_use]
    pub fn loops(&self) -> Vec<generic::Loop> {
        let mut map = std::collections::BTreeMap::new();

        // First, insert all cue from the `Serato Markers2` tag into the map.
        if let Some(m) = &self.markers2 {
            for saved_loop in m.loops() {
                map.insert(saved_loop.index, saved_loop.to_owned());
            }
        }

        // Now, iterate over the cue markers from the `Serato Markers_` tag.
        if let Some(m) = &self.markers {
            for (index, marker) in m.loops() {
                if marker.marker_type != markers::MarkerType::Loop {
                    // This can only happen is `Markers::cues()` returns non-cue markers, which
                    // would be a bug.
                    // FIXME: Throw error here?
                    continue;
                }

                if marker.start_position == None || marker.end_position == None {
                    // This may happen even for valid data, because unset loops lack the start/end
                    // position.
                    map.remove(&index);
                    continue;
                }

                let start_position = marker.start_position.unwrap();
                let end_position = marker.end_position.unwrap();

                // If the loop is set in both `Serato Markers2` and `Serato Markers_`, use
                // the version from `Serato Markers_`, but keep the label from `Serato
                // Markers2` because the `Serato Markers_` tag doesn't contain labels.
                let markers2_loop = map.remove(&index);
                let label = match markers2_loop {
                    Some(c) => c.label,
                    None => String::new(),
                };

                map.insert(
                    index,
                    generic::Loop {
                        index,
                        start_position,
                        end_position,
                        color: marker.color,
                        label,
                        is_locked: marker.is_locked,
                    },
                );
            }
        }

        // Return the sorted list of cues.
        map.into_values().collect()
    }

    /// Returns [flips](https://serato.com/dj/pro/expansions/flip) from the [`Serato Markers2`](Markers2) tag.
    pub fn flips(&self) -> Option<impl Iterator<Item = &generic::Flip>> {
        self.markers2.as_ref().map(Markers2::flips)
    }

    /// Returns the track color from the [`Serato Markers_`](Markers) and [`Serato
    /// Markers2`](Markers2) tags.
    ///
    /// If present the color in `Serato Markers_` takes precedence over that in
    /// `Serato Markers2`. This is what Serato does too, i.e. if `Serato Markers_`
    /// and `Serato Markers2` contradict each other, Serato will use the value
    /// from `Serato Markers_`.
    #[must_use]
    pub fn track_color(&self) -> Option<Color> {
        self.markers
            .as_ref()
            .map(Markers::track_color)
            .or_else(|| self.markers2.as_ref().and_then(Markers2::track_color))
    }

    /// Returns the waveform overview data color from the [`Serato Overview`](Overview) tag.
    #[must_use]
    pub fn overview_data(&self) -> Option<&[Vec<u8>]> {
        self.overview.as_ref().map(|overview| &overview.data[..])
    }
}

impl Default for TagContainer {
    fn default() -> Self {
        Self::new()
    }
}