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}