Skip to main content

trackparse/
types.rs

1//! Public types. They mirror `spec/schema/parsed-track.schema.json`; field names are snake_case
2//! in Rust and the spec's camelCase in JSON (`to_json()`, and `serde` with that feature).
3
4use crate::json::Json;
5
6// Decision: output structs stay constructible (not `#[non_exhaustive]`): tests build them.
7
8/// The error of `FromStr` on the crate's enums: the string is not one of the spec's names.
9///
10/// ```
11/// use trackparse::{ArtistRole, UnknownName};
12///
13/// let err: UnknownName = "singer".parse::<ArtistRole>().unwrap_err();
14/// assert_eq!(err.to_string(), "unknown ArtistRole name: \"singer\"");
15/// ```
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct UnknownName {
18    pub(crate) kind: &'static str,
19    pub(crate) name: String,
20}
21
22impl UnknownName {
23    /// The string that was not recognised.
24    pub fn name(&self) -> &str {
25        &self.name
26    }
27}
28
29impl std::fmt::Display for UnknownName {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "unknown {} name: {:?}", self.kind, self.name)
32    }
33}
34
35impl std::error::Error for UnknownName {}
36
37/// Declares a string-valued enum: `as_str()`, `from_name()`, `ALL`, `Display`, `FromStr`, and
38/// with the `serde` feature `Serialize` / `Deserialize` as the spec name.
39macro_rules! string_enum {
40    ($(#[$meta:meta])* $name:ident { $($variant:ident => $text:literal),+ $(,)? }) => {
41        $(#[$meta])*
42        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43        pub enum $name {
44            $(
45                #[doc = concat!("`", stringify!($text), "` in the spec and in JSON.")]
46                $variant
47            ),+
48        }
49
50        impl $name {
51            /// Every value, in the spec's order.
52            pub const ALL: &'static [$name] = &[$($name::$variant),+];
53
54            /// Every name, in the order of `ALL`.
55            pub const NAMES: &'static [&'static str] = &[$($text),+];
56
57            /// The name used in the spec and in JSON.
58            pub fn as_str(self) -> &'static str {
59                match self { $($name::$variant => $text),+ }
60            }
61
62            /// The value with that spec name (exact match), or `None`.
63            pub fn from_name(name: &str) -> Option<$name> {
64                match name { $($text => Some($name::$variant),)+ _ => None }
65            }
66        }
67
68        impl std::fmt::Display for $name {
69            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70                f.write_str(self.as_str())
71            }
72        }
73
74        impl std::str::FromStr for $name {
75            type Err = $crate::types::UnknownName;
76
77            fn from_str(name: &str) -> Result<$name, Self::Err> {
78                $name::from_name(name).ok_or_else(|| $crate::types::UnknownName {
79                    kind: stringify!($name),
80                    name: name.to_string(),
81                })
82            }
83        }
84
85        #[cfg(feature = "serde")]
86        #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
87        impl serde::Serialize for $name {
88            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
89                serializer.serialize_str(self.as_str())
90            }
91        }
92
93        #[cfg(feature = "serde")]
94        #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
95        impl<'de> serde::Deserialize<'de> for $name {
96            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<$name, D::Error> {
97                let name = <String as serde::Deserialize>::deserialize(deserializer)?;
98                $name::from_name(&name)
99                    .ok_or_else(|| serde::de::Error::unknown_variant(&name, $name::NAMES))
100            }
101        }
102    };
103}
104pub(crate) use string_enum;
105
106/// Adds the inherent `to_json()` to types that implement [`ToJson`], so no import is needed, and
107/// marks them as the crate's own (`ToJson` is sealed).
108macro_rules! json_methods {
109    ($($name:ty),+ $(,)?) => {
110        $(
111            impl $crate::types::sealed::Sealed for $name {}
112
113            impl $name {
114                /// The spec's JSON form as compact text: camelCase keys, every key present, in
115                /// the schema's order. The same text the JavaScript and Python packages give.
116                pub fn to_json(&self) -> String {
117                    $crate::types::ToJson::to_json_value(self).write()
118                }
119            }
120        )+
121    };
122}
123pub(crate) use json_methods;
124
125string_enum! {
126    /// The mode a string was parsed in (R2). `Tracklist` is only used when asked for (R13.6);
127    /// `auto` never resolves to it.
128    ///
129    /// ```
130    /// use trackparse::{parse, Mode};
131    ///
132    /// assert_eq!(parse("Noisia - Stigma").mode, Mode::Clean);
133    /// assert_eq!(parse("Noisia - Stigma (Official Video)").mode, Mode::Youtube);
134    /// assert_eq!(parse("01_noisia_-_stigma.mp3").mode, Mode::Filename);
135    /// assert_eq!(Mode::Youtube.as_str(), "youtube");
136    /// assert_eq!("filename".parse::<Mode>(), Ok(Mode::Filename));
137    /// ```
138    #[non_exhaustive]
139    Mode { Clean => "clean", Youtube => "youtube", Filename => "filename", Tracklist => "tracklist" }
140}
141
142string_enum! {
143    /// What an artist did on the track (R7).
144    ///
145    /// ```
146    /// use trackparse::{parse, ArtistRole};
147    ///
148    /// let track = parse("Noisia feat. Foreign Beggars - Shellshock (prod. by Phace)");
149    /// let roles: Vec<ArtistRole> = track.artists.iter().map(|a| a.role).collect();
150    /// assert_eq!(roles, [ArtistRole::Primary, ArtistRole::Featured, ArtistRole::Producer]);
151    /// assert_eq!(ArtistRole::Featured.to_string(), "featured");
152    /// ```
153    #[non_exhaustive]
154    ArtistRole { Primary => "primary", Featured => "featured", Remixer => "remixer", Producer => "producer" }
155}
156
157string_enum! {
158    /// Where in the string an artist was found: the artist side, the title side, or a version
159    /// group.
160    ///
161    /// ```
162    /// use trackparse::{parse, ArtistSource};
163    ///
164    /// let track = parse("Sub Focus - Out the Blue ft. Alice Gold");
165    /// assert_eq!(track.artists[0].source, ArtistSource::Artist);
166    /// assert_eq!(track.artists[1].source, ArtistSource::Title);
167    /// ```
168    #[non_exhaustive]
169    ArtistSource { Artist => "artist", Title => "title", Version => "version" }
170}
171
172string_enum! {
173    /// The type of a version (R5): remix, edit, live, remaster and so on.
174    ///
175    /// ```
176    /// use trackparse::{parse, VersionType};
177    ///
178    /// let track = parse("Fox Stevenson - Bruises (Magnetude Remix) [Extended Mix]");
179    /// let types: Vec<VersionType> = track.versions.iter().map(|v| v.version_type).collect();
180    /// assert_eq!(types, [VersionType::Remix, VersionType::Extended]);
181    /// assert_eq!(VersionType::SpedUp.as_str(), "spedUp");
182    /// ```
183    #[non_exhaustive]
184    VersionType {
185        Remix => "remix", Bootleg => "bootleg", Vip => "vip", Edit => "edit", Flip => "flip",
186        Refix => "refix", Rework => "rework", Mashup => "mashup", Blend => "blend", Dub => "dub",
187        Mix => "mix", Extended => "extended", Radio => "radio", Club => "club",
188        Original => "original", Instrumental => "instrumental", Acapella => "acapella",
189        Live => "live", Acoustic => "acoustic", Remaster => "remaster", Demo => "demo",
190        Reprise => "reprise", Cover => "cover", Version => "version", SpedUp => "spedUp",
191        Slowed => "slowed", Nightcore => "nightcore",
192    }
193}
194
195string_enum! {
196    /// How a version was written: in a bracket group, or after a dash.
197    ///
198    /// ```
199    /// use trackparse::{parse, VersionDelimiter};
200    ///
201    /// let track = parse("Noisia - Stigma [VIP] - Extended Mix");
202    /// assert_eq!(track.versions[0].delimiter, VersionDelimiter::Square);
203    /// assert_eq!(track.versions[1].delimiter, VersionDelimiter::Dash);
204    /// ```
205    VersionDelimiter { Paren => "(", Square => "[", Curly => "{", Dash => "-" }
206}
207
208string_enum! {
209    /// What kind of noise a junk fragment is (R5).
210    ///
211    /// ```
212    /// use trackparse::{parse, JunkKind};
213    ///
214    /// let track = parse("Noisia - Stigma [Official Video] [HD] [Free Download]");
215    /// let kinds: Vec<JunkKind> = track.junk.iter().map(|j| j.kind).collect();
216    /// assert_eq!(kinds, [JunkKind::Video, JunkKind::Quality, JunkKind::Promo]);
217    /// ```
218    #[non_exhaustive]
219    JunkKind {
220        Video => "video", Audio => "audio", Lyrics => "lyrics", Quality => "quality",
221        Promo => "promo", Platform => "platform", Label => "label", Genre => "genre",
222        Other => "other",
223    }
224}
225
226string_enum! {
227    /// A warning of the single-track parser: the result is a guess the caller may want to check.
228    ///
229    /// ```
230    /// use trackparse::{parse, Warning};
231    ///
232    /// assert_eq!(parse("Just A Title").warnings, [Warning::NoSeparator]);
233    /// assert_eq!(parse("Noisia - Stigma (Unbalanced").warnings, [Warning::UnbalancedBrackets]);
234    /// assert_eq!(Warning::NoSeparator.as_str(), "noSeparator");
235    /// ```
236    #[non_exhaustive]
237    Warning {
238        NoSeparator => "noSeparator", AmbiguousSeparator => "ambiguousSeparator",
239        UnspacedDashSplit => "unspacedDashSplit", AsymmetricDashSplit => "asymmetricDashSplit",
240        BySplit => "bySplit", QuotedTitleSplit => "quotedTitleSplit",
241        AmbiguousMixCredit => "ambiguousMixCredit", UnbalancedBrackets => "unbalancedBrackets",
242    }
243}
244
245/// One credited artist.
246///
247/// ```
248/// use trackparse::{parse, ArtistRole, ArtistSource};
249///
250/// let track = parse("Noisia & The Upbeats - Dead Limit");
251/// let second = &track.artists[1];
252/// assert_eq!(second.name, "The Upbeats");
253/// assert_eq!(second.role, ArtistRole::Primary);
254/// assert_eq!(second.joiner.as_deref(), Some("&"));
255/// assert_eq!(second.source, ArtistSource::Artist);
256/// assert_eq!(
257///     second.to_json(),
258///     r#"{"name":"The Upbeats","role":"primary","joiner":"&","source":"artist"}"#
259/// );
260/// ```
261#[derive(Debug, Clone, PartialEq, Eq, Hash)]
262#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
263pub struct Artist {
264    /// The name as written, trimmed.
265    pub name: String,
266    /// What the artist did on the track.
267    pub role: ArtistRole,
268    /// Raw joiner / marker text that preceded the name in its credit list; `None` for the first.
269    pub joiner: Option<String>,
270    /// Where in the string the artist was found.
271    pub source: ArtistSource,
272}
273
274/// One version of the track: a remix, an edit, a live take, a remaster (R5, R8).
275///
276/// ```
277/// use trackparse::{parse, VersionDelimiter, VersionType};
278///
279/// let track = parse("Fox Stevenson - Bruises (Magnetude Extended Remix) [Live at Lowlands 2016]");
280/// let remix = &track.versions[0];
281/// assert_eq!(remix.version_type, VersionType::Remix);
282/// assert_eq!(remix.raw, "Magnetude Extended Remix");
283/// assert_eq!(remix.artists[0].name, "Magnetude");
284/// assert_eq!(remix.modifiers, ["extended"]);
285/// assert_eq!(remix.delimiter, VersionDelimiter::Paren);
286/// let live = &track.versions[1];
287/// assert_eq!(live.version_type, VersionType::Live);
288/// assert_eq!(live.descriptor.as_deref(), Some("at Lowlands"));
289/// assert_eq!(live.year, Some(2016));
290/// assert_eq!(live.delimiter, VersionDelimiter::Square);
291/// ```
292#[derive(Debug, Clone, PartialEq, Eq, Hash)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
295pub struct Version {
296    /// The type; `type` in JSON.
297    #[cfg_attr(feature = "serde", serde(rename = "type"))]
298    pub version_type: VersionType,
299    /// The version text as written, without its brackets.
300    pub raw: String,
301    /// Who made it (role `Remixer`); empty for versions without a credit.
302    pub artists: Vec<Artist>,
303    /// Other version words in the same group, as lowercase keys (`extended` in `Extended Remix`).
304    pub modifiers: Vec<String>,
305    /// Free text of the version that is neither a credit nor a keyword.
306    pub descriptor: Option<String>,
307    /// A year written inside the version.
308    pub year: Option<u32>,
309    /// The credit is a placeholder such as `ID` or `Unknown`.
310    pub unknown_artist: bool,
311    /// How the version was written.
312    pub delimiter: VersionDelimiter,
313}
314
315/// A fragment that says nothing about the track: `Official Video`, `HD`, `Free Download`.
316/// It is taken out of the title and kept here (the parser extracts, it does not delete).
317///
318/// ```
319/// use trackparse::{parse, JunkKind};
320///
321/// let track = parse("Noisia - Stigma [Official Video]");
322/// assert_eq!(track.junk[0].raw, "Official Video");
323/// assert_eq!(track.junk[0].kind, JunkKind::Video);
324/// ```
325#[derive(Debug, Clone, PartialEq, Eq, Hash)]
326#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
327pub struct Junk {
328    /// The fragment as written, without its brackets.
329    pub raw: String,
330    /// What kind of noise it is.
331    pub kind: JunkKind,
332}
333
334/// Yes/no facts about the track.
335///
336/// ```
337/// let track = trackparse::parse("ID - ID (Explicit)");
338/// assert!(track.flags.explicit);
339/// assert!(track.flags.unknown_artist && track.flags.unknown_title);
340/// assert!(!track.flags.clean);
341/// ```
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
343#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
344#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
345pub struct Flags {
346    /// Marked `Explicit` / `Dirty`.
347    pub explicit: bool,
348    /// Marked `Clean`.
349    pub clean: bool,
350    /// The artist is a placeholder (`ID`, `Unknown Artist`).
351    pub unknown_artist: bool,
352    /// The title is a placeholder (`ID`, `Untitled`).
353    pub unknown_title: bool,
354}
355
356/// A track number in front of the string (R4.2).
357///
358/// ```
359/// let track = trackparse::parse("01. Camo & Krooked - Climax");
360/// let position = track.position.unwrap();
361/// assert_eq!(position.raw, "01");
362/// assert_eq!(position.number, 1);
363/// ```
364#[derive(Debug, Clone, PartialEq, Eq, Hash)]
365#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
366pub struct Position {
367    /// As written, without the separator after it.
368    pub raw: String,
369    /// The number in it.
370    pub number: u32,
371}
372
373/// A cue time in front of a single track string (R4.1), in whole seconds.
374///
375/// ```
376/// let track = trackparse::parse("[01:23:45] Sub Focus - Out the Blue");
377/// let timestamp = track.timestamp.unwrap();
378/// assert_eq!(timestamp.raw, "01:23:45");
379/// assert_eq!(timestamp.seconds, 5025);
380/// ```
381#[derive(Debug, Clone, PartialEq, Eq, Hash)]
382#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
383pub struct Timestamp {
384    /// As written, without brackets.
385    pub raw: String,
386    /// The time in seconds.
387    pub seconds: u64,
388}
389
390/// The result of [`parse`](crate::parse): everything recognised in one track string.
391///
392/// Anything recognised is in a field; anything not recognised stays in `title` verbatim.
393///
394/// ```
395/// let track = trackparse::parse("Noisia - Stigma (2015)");
396/// assert_eq!(
397///     track.to_json(),
398///     concat!(
399///         r#"{"input":"Noisia - Stigma (2015)","mode":"clean","position":null,"#,
400///         r#""timestamp":null,"artists":[{"name":"Noisia","role":"primary","joiner":null,"#,
401///         r#""source":"artist"}],"title":"Stigma","fullTitle":"Stigma","versions":[],"#,
402///         r#""year":2015,"flags":{"explicit":false,"clean":false,"unknownArtist":false,"#,
403///         r#""unknownTitle":false},"junk":[],"warnings":[]}"#
404///     )
405/// );
406/// ```
407#[derive(Debug, Clone, PartialEq, Eq, Hash)]
408#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
409#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
410pub struct ParsedTrack {
411    /// The string that was parsed, unchanged.
412    pub input: String,
413    /// The mode it was parsed in.
414    pub mode: Mode,
415    /// A leading track number.
416    pub position: Option<Position>,
417    /// A leading cue time.
418    pub timestamp: Option<Timestamp>,
419    /// Primary, featured and producer credits, in the order written. Remixers are in
420    /// `versions`; [`all_artists`](crate::all_artists) gives both.
421    pub artists: Vec<Artist>,
422    /// The title without versions, featured credits and junk.
423    pub title: String,
424    /// The title with its versions, as written (`fullTitle` in JSON).
425    pub full_title: String,
426    /// Remixes, edits and other versions, in the order written.
427    pub versions: Vec<Version>,
428    /// A release year found outside the versions.
429    pub year: Option<u32>,
430    /// Yes/no facts.
431    pub flags: Flags,
432    /// Fragments that say nothing about the track.
433    pub junk: Vec<Junk>,
434    /// Guesses the caller may want to check.
435    pub warnings: Vec<Warning>,
436}
437
438json_methods!(Artist, Version, Junk, Flags, Position, Timestamp, ParsedTrack);
439
440/// Conversion to the spec's JSON form (camelCase keys, every key present, schema order).
441///
442/// Every output type also has `to_json()` as an inherent method; the trait is for generic code.
443/// It is sealed: only the types of this crate implement it.
444///
445/// ```
446/// use trackparse::json::Json;
447/// use trackparse::ToJson;
448///
449/// let track = trackparse::parse("Noisia - Stigma");
450/// let value: Json = track.to_json_value();
451/// assert_eq!(value.get("title").and_then(Json::as_str), Some("Stigma"));
452/// ```
453pub trait ToJson: sealed::Sealed {
454    /// The value as a JSON tree.
455    fn to_json_value(&self) -> Json;
456
457    /// Compact JSON text.
458    fn to_json(&self) -> String {
459        self.to_json_value().write()
460    }
461}
462
463pub(crate) mod sealed {
464    /// Keeps [`ToJson`](super::ToJson) closed to this crate's types.
465    pub trait Sealed {}
466}
467
468pub(crate) fn obj(entries: Vec<(&str, Json)>) -> Json {
469    Json::Obj(entries.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
470}
471
472pub(crate) fn list<T: ToJson>(items: &[T]) -> Json {
473    Json::Arr(items.iter().map(ToJson::to_json_value).collect())
474}
475
476pub(crate) fn opt<T: ToJson>(v: &Option<T>) -> Json {
477    v.as_ref().map_or(Json::Null, ToJson::to_json_value)
478}
479
480fn num_opt(v: Option<u32>) -> Json {
481    v.map_or(Json::Null, |n| Json::Num(f64::from(n)))
482}
483
484impl ToJson for Artist {
485    fn to_json_value(&self) -> Json {
486        obj(vec![
487            ("name", Json::str(&self.name)),
488            ("role", Json::str(self.role.as_str())),
489            ("joiner", Json::opt_str(self.joiner.as_deref())),
490            ("source", Json::str(self.source.as_str())),
491        ])
492    }
493}
494
495impl ToJson for Version {
496    fn to_json_value(&self) -> Json {
497        obj(vec![
498            ("type", Json::str(self.version_type.as_str())),
499            ("raw", Json::str(&self.raw)),
500            ("artists", list(&self.artists)),
501            ("modifiers", Json::Arr(self.modifiers.iter().map(|m| Json::str(m)).collect())),
502            ("descriptor", Json::opt_str(self.descriptor.as_deref())),
503            ("year", num_opt(self.year)),
504            ("unknownArtist", Json::Bool(self.unknown_artist)),
505            ("delimiter", Json::str(self.delimiter.as_str())),
506        ])
507    }
508}
509
510impl ToJson for Junk {
511    fn to_json_value(&self) -> Json {
512        obj(vec![("raw", Json::str(&self.raw)), ("kind", Json::str(self.kind.as_str()))])
513    }
514}
515
516impl ToJson for Flags {
517    fn to_json_value(&self) -> Json {
518        obj(vec![
519            ("explicit", Json::Bool(self.explicit)),
520            ("clean", Json::Bool(self.clean)),
521            ("unknownArtist", Json::Bool(self.unknown_artist)),
522            ("unknownTitle", Json::Bool(self.unknown_title)),
523        ])
524    }
525}
526
527impl ToJson for Position {
528    fn to_json_value(&self) -> Json {
529        obj(vec![("raw", Json::str(&self.raw)), ("number", Json::Num(f64::from(self.number)))])
530    }
531}
532
533impl ToJson for Timestamp {
534    fn to_json_value(&self) -> Json {
535        obj(vec![("raw", Json::str(&self.raw)), ("seconds", Json::Num(self.seconds as f64))])
536    }
537}
538
539impl ToJson for ParsedTrack {
540    fn to_json_value(&self) -> Json {
541        obj(vec![
542            ("input", Json::str(&self.input)),
543            ("mode", Json::str(self.mode.as_str())),
544            ("position", opt(&self.position)),
545            ("timestamp", opt(&self.timestamp)),
546            ("artists", list(&self.artists)),
547            ("title", Json::str(&self.title)),
548            ("fullTitle", Json::str(&self.full_title)),
549            ("versions", list(&self.versions)),
550            ("year", num_opt(self.year)),
551            ("flags", self.flags.to_json_value()),
552            ("junk", list(&self.junk)),
553            ("warnings", Json::Arr(self.warnings.iter().map(|w| Json::str(w.as_str())).collect())),
554        ])
555    }
556}
557
558// ---------------------------------------------------------------------------
559// The pipeline's own track. Version types and junk kinds are strings here: through loose JSON
560// options (`keywords.versionHeads`, `keywords.junk`) the JS reference passes any string value on
561// to the output, which the typed enums above cannot hold. `wire` writes this form; the typed API
562// converts it (the typed options can only name real types and kinds).
563
564#[derive(Debug, Clone, PartialEq, Eq)]
565pub(crate) struct RawVersion {
566    pub vtype: String,
567    pub raw: String,
568    pub artists: Vec<Artist>,
569    pub modifiers: Vec<String>,
570    pub descriptor: Option<String>,
571    pub year: Option<u32>,
572    pub unknown_artist: bool,
573    pub delimiter: VersionDelimiter,
574}
575
576#[derive(Debug, Clone, PartialEq, Eq)]
577pub(crate) struct RawJunk {
578    pub raw: String,
579    pub kind: String,
580}
581
582#[derive(Debug, Clone, PartialEq, Eq)]
583pub(crate) struct RawTrack {
584    pub input: String,
585    pub mode: Mode,
586    pub position: Option<Position>,
587    pub timestamp: Option<Timestamp>,
588    pub artists: Vec<Artist>,
589    pub title: String,
590    pub full_title: String,
591    pub versions: Vec<RawVersion>,
592    pub year: Option<u32>,
593    pub flags: Flags,
594    pub junk: Vec<RawJunk>,
595    pub warnings: Vec<Warning>,
596}
597
598impl RawTrack {
599    pub(crate) fn empty(input: &str, mode: Mode) -> RawTrack {
600        RawTrack {
601            input: input.to_string(),
602            mode,
603            position: None,
604            timestamp: None,
605            artists: Vec::new(),
606            title: String::new(),
607            full_title: String::new(),
608            versions: Vec::new(),
609            year: None,
610            flags: Flags::default(),
611            junk: Vec::new(),
612            warnings: Vec::new(),
613        }
614    }
615
616    /// The typed track. A type or kind name the enums do not have (only reachable through loose
617    /// JSON options) becomes `VersionType::Version` / `JunkKind::Other`.
618    pub(crate) fn into_track(self) -> ParsedTrack {
619        ParsedTrack {
620            input: self.input,
621            mode: self.mode,
622            position: self.position,
623            timestamp: self.timestamp,
624            artists: self.artists,
625            title: self.title,
626            full_title: self.full_title,
627            versions: self
628                .versions
629                .into_iter()
630                .map(|v| Version {
631                    version_type: VersionType::from_name(&v.vtype).unwrap_or(VersionType::Version),
632                    raw: v.raw,
633                    artists: v.artists,
634                    modifiers: v.modifiers,
635                    descriptor: v.descriptor,
636                    year: v.year,
637                    unknown_artist: v.unknown_artist,
638                    delimiter: v.delimiter,
639                })
640                .collect(),
641            year: self.year,
642            flags: self.flags,
643            junk: self
644                .junk
645                .into_iter()
646                .map(|j| Junk {
647                    raw: j.raw,
648                    kind: JunkKind::from_name(&j.kind).unwrap_or(JunkKind::Other),
649                })
650                .collect(),
651            warnings: self.warnings,
652        }
653    }
654
655    /// The spec's JSON form, written exactly as the JS reference would.
656    pub(crate) fn to_json_value(&self) -> Json {
657        let versions = self
658            .versions
659            .iter()
660            .map(|v| {
661                obj(vec![
662                    ("type", Json::str(&v.vtype)),
663                    ("raw", Json::str(&v.raw)),
664                    ("artists", list(&v.artists)),
665                    ("modifiers", Json::Arr(v.modifiers.iter().map(|m| Json::str(m)).collect())),
666                    ("descriptor", Json::opt_str(v.descriptor.as_deref())),
667                    ("year", num_opt(v.year)),
668                    ("unknownArtist", Json::Bool(v.unknown_artist)),
669                    ("delimiter", Json::str(v.delimiter.as_str())),
670                ])
671            })
672            .collect();
673        let junk = self
674            .junk
675            .iter()
676            .map(|j| obj(vec![("raw", Json::str(&j.raw)), ("kind", Json::str(&j.kind))]))
677            .collect();
678        obj(vec![
679            ("input", Json::str(&self.input)),
680            ("mode", Json::str(self.mode.as_str())),
681            ("position", opt(&self.position)),
682            ("timestamp", opt(&self.timestamp)),
683            ("artists", list(&self.artists)),
684            ("title", Json::str(&self.title)),
685            ("fullTitle", Json::str(&self.full_title)),
686            ("versions", Json::Arr(versions)),
687            ("year", num_opt(self.year)),
688            ("flags", self.flags.to_json_value()),
689            ("junk", Json::Arr(junk)),
690            ("warnings", Json::Arr(self.warnings.iter().map(|w| Json::str(w.as_str())).collect())),
691        ])
692    }
693}