disco_quick/
release.rs

1use crate::artist_credit::{
2    get_credit_string, ArtistCredit, ArtistCreditBuilder, ArtistCreditParser,
3};
4use crate::company::{CompanyParser, ReleaseCompany};
5use crate::parser::{Parser, ParserError};
6use crate::reader::XmlReader;
7use crate::shared::Image;
8use crate::track::{Track, TrackParser};
9use crate::util::{find_attr, find_attr_optional, maybe_text};
10use crate::video::{Video, VideoParser};
11use log::debug;
12use quick_xml::events::Event;
13use std::fmt;
14use std::mem::take;
15
16#[derive(Clone, Debug, Default, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Release {
19    pub id: u32,
20    pub status: String,
21    pub title: String,
22    pub artists: Vec<ArtistCredit>,
23    pub country: String,
24    pub labels: Vec<ReleaseLabel>,
25    pub series: Vec<ReleaseLabel>,
26    pub released: String,
27    pub notes: Option<String>,
28    pub genres: Vec<String>,
29    pub styles: Vec<String>,
30    pub master_id: Option<u32>,
31    pub is_main_release: bool,
32    pub data_quality: String,
33    pub images: Vec<Image>,
34    pub videos: Vec<Video>,
35    pub extraartists: Vec<ArtistCredit>,
36    pub tracklist: Vec<Track>,
37    pub formats: Vec<ReleaseFormat>,
38    pub companies: Vec<ReleaseCompany>,
39    pub identifiers: Vec<ReleaseIdentifier>,
40}
41
42impl Release {
43    pub fn builder(id: u32, title: &str) -> ReleaseBuilder {
44        ReleaseBuilder {
45            inner: Release {
46                id,
47                title: title.to_string(),
48                ..Default::default()
49            },
50        }
51    }
52}
53
54#[derive(Clone, Debug, Default, PartialEq, Eq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub struct ReleaseLabel {
57    pub id: Option<u32>,
58    pub name: String,
59    pub catno: Option<String>,
60}
61
62#[derive(Clone, Debug, Default, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub struct ReleaseFormat {
65    pub qty: String, // https://www.discogs.com/release/8262262
66    pub name: String,
67    pub text: Option<String>,
68    pub descriptions: Vec<String>,
69}
70
71#[derive(Clone, Debug, Default, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub struct ReleaseIdentifier {
74    pub r#type: String,
75    pub description: Option<String>,
76    pub value: Option<String>,
77}
78
79impl fmt::Display for Release {
80    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
81        let artist_credit = get_credit_string(&self.artists);
82        write!(f, "{} - {}", artist_credit, self.title)
83    }
84}
85
86pub struct ReleasesReader {
87    buf: Vec<u8>,
88    reader: XmlReader,
89    parser: ReleaseParser,
90}
91
92impl ReleasesReader {
93    pub fn new(reader: XmlReader, buf: Vec<u8>) -> Self {
94        Self {
95            buf,
96            reader,
97            parser: ReleaseParser::new(),
98        }
99    }
100}
101
102impl Iterator for ReleasesReader {
103    type Item = Release;
104    fn next(&mut self) -> Option<Self::Item> {
105        loop {
106            match self.reader.read_event_into(&mut self.buf).unwrap() {
107                Event::Eof => {
108                    return None;
109                }
110                ev => self.parser.process(&ev).unwrap(),
111            };
112            if self.parser.item_ready {
113                return Some(self.parser.take());
114            }
115            self.buf.clear();
116        }
117    }
118}
119
120#[derive(Debug, Default)]
121enum ParserState {
122    #[default]
123    Release,
124    Title,
125    Country,
126    Released,
127    Notes,
128    Genres,
129    Styles,
130    MasterId,
131    DataQuality,
132    Labels,
133    Series,
134    Videos,
135    Artists,
136    ExtraArtists,
137    TrackList,
138    Format,
139    Companies,
140    Identifiers,
141    Images,
142}
143
144#[derive(Debug, Default)]
145pub struct ReleaseParser {
146    state: ParserState,
147    current_item: Release,
148    artist_parser: ArtistCreditParser,
149    video_parser: VideoParser,
150    track_parser: TrackParser,
151    company_parser: CompanyParser,
152    item_ready: bool,
153}
154
155impl Parser for ReleaseParser {
156    type Item = Release;
157
158    fn new() -> Self {
159        Self::default()
160    }
161
162    fn take(&mut self) -> Release {
163        self.item_ready = false;
164        take(&mut self.current_item)
165    }
166
167    fn process(&mut self, ev: &Event) -> Result<(), ParserError> {
168        self.state = match self.state {
169            ParserState::Release => match ev {
170                Event::End(e) if e.local_name().as_ref() == b"release" => {
171                    self.item_ready = true;
172                    ParserState::Release
173                }
174                Event::Start(e) if e.local_name().as_ref() == b"release" => {
175                    self.current_item.id = find_attr(e, b"id")?.parse()?;
176                    debug!("Began parsing Release {}", self.current_item.id);
177                    self.current_item.status = find_attr(e, b"status")?.to_string();
178                    ParserState::Release
179                }
180                Event::Start(e) if e.local_name().as_ref() == b"master_id" => {
181                    self.current_item.is_main_release =
182                        find_attr(e, b"is_main_release")?.parse()?;
183                    ParserState::MasterId
184                }
185                Event::Start(e) => match e.local_name().as_ref() {
186                    b"title" => ParserState::Title,
187                    b"country" => ParserState::Country,
188                    b"released" => ParserState::Released,
189                    b"notes" => ParserState::Notes,
190                    b"genres" => ParserState::Genres,
191                    b"styles" => ParserState::Styles,
192                    b"data_quality" => ParserState::DataQuality,
193                    b"labels" => ParserState::Labels,
194                    b"series" => ParserState::Series,
195                    b"videos" => ParserState::Videos,
196                    b"artists" => ParserState::Artists,
197                    b"extraartists" => ParserState::ExtraArtists,
198                    b"tracklist" => ParserState::TrackList,
199                    b"formats" => ParserState::Format,
200                    b"identifiers" => ParserState::Identifiers,
201                    b"companies" => ParserState::Companies,
202                    b"images" => ParserState::Images,
203                    _ => ParserState::Release,
204                },
205                _ => ParserState::Release,
206            },
207
208            ParserState::Title => match ev {
209                Event::Text(e) => {
210                    self.current_item.title = e.unescape()?.to_string();
211                    ParserState::Title
212                }
213                _ => ParserState::Release,
214            },
215
216            ParserState::Country => match ev {
217                Event::Text(e) => {
218                    self.current_item.country = e.unescape()?.to_string();
219                    ParserState::Country
220                }
221                _ => ParserState::Release,
222            },
223
224            ParserState::Released => match ev {
225                Event::Text(e) => {
226                    self.current_item.released = e.unescape()?.to_string();
227                    ParserState::Released
228                }
229                _ => ParserState::Release,
230            },
231
232            ParserState::Notes => match ev {
233                Event::Text(e) => {
234                    self.current_item.notes = maybe_text(e)?;
235                    ParserState::Notes
236                }
237                _ => ParserState::Release,
238            },
239
240            ParserState::Artists => match ev {
241                Event::End(e) if e.local_name().as_ref() == b"artists" => ParserState::Release,
242
243                ev => {
244                    self.artist_parser.process(ev)?;
245                    if self.artist_parser.item_ready {
246                        self.current_item.artists.push(self.artist_parser.take());
247                    }
248                    ParserState::Artists
249                }
250            },
251
252            ParserState::ExtraArtists => match ev {
253                Event::End(e) if e.local_name().as_ref() == b"extraartists" => ParserState::Release,
254
255                ev => {
256                    self.artist_parser.process(ev)?;
257                    if self.artist_parser.item_ready {
258                        let ea = self.artist_parser.take();
259                        self.current_item.extraartists.push(ea);
260                    }
261                    ParserState::ExtraArtists
262                }
263            },
264
265            ParserState::Genres => match ev {
266                Event::End(e) if e.local_name().as_ref() == b"genres" => ParserState::Release,
267
268                Event::Text(e) => {
269                    self.current_item.genres.push(e.unescape()?.to_string());
270                    ParserState::Genres
271                }
272                _ => ParserState::Genres,
273            },
274
275            ParserState::Styles => match ev {
276                Event::End(e) if e.local_name().as_ref() == b"styles" => ParserState::Release,
277
278                Event::Text(e) => {
279                    self.current_item.styles.push(e.unescape()?.to_string());
280                    ParserState::Styles
281                }
282                _ => ParserState::Styles,
283            },
284
285            ParserState::Format => match ev {
286                Event::Start(e) if e.local_name().as_ref() == b"format" => {
287                    let format = ReleaseFormat {
288                        name: find_attr(e, b"name")?.to_string(),
289                        qty: find_attr(e, b"qty")?.to_string(),
290                        text: find_attr_optional(e, b"text")?.map(|t| t.to_string()),
291                        ..Default::default()
292                    };
293                    self.current_item.formats.push(format);
294                    ParserState::Format
295                }
296                Event::Text(e) => {
297                    let description = e.unescape()?.to_string();
298                    let Some(format) = self.current_item.formats.last_mut() else {
299                        return Err(ParserError::MissingData("Release format"));
300                    };
301                    format.descriptions.push(description);
302                    ParserState::Format
303                }
304                Event::End(e) if e.local_name().as_ref() == b"formats" => ParserState::Release,
305
306                _ => ParserState::Format,
307            },
308
309            ParserState::Identifiers => match ev {
310                Event::Empty(e) => {
311                    let identifier = ReleaseIdentifier {
312                        r#type: find_attr(e, b"type")?.to_string(),
313                        description: find_attr_optional(e, b"description")?.map(|d| d.to_string()),
314                        value: find_attr_optional(e, b"value")?.map(|v| v.to_string()),
315                    };
316                    self.current_item.identifiers.push(identifier);
317                    ParserState::Identifiers
318                }
319                _ => ParserState::Release,
320            },
321
322            ParserState::MasterId => match ev {
323                Event::Text(e) => {
324                    self.current_item.master_id = Some(e.unescape()?.parse()?);
325                    ParserState::MasterId
326                }
327                Event::End(_) => ParserState::Release,
328
329                _ => ParserState::MasterId,
330            },
331
332            ParserState::DataQuality => match ev {
333                Event::Text(e) => {
334                    self.current_item.data_quality = e.unescape()?.to_string();
335                    ParserState::DataQuality
336                }
337                _ => ParserState::Release,
338            },
339
340            ParserState::Labels => match ev {
341                Event::Empty(e) => {
342                    let id = match find_attr_optional(e, b"id")? {
343                        Some(id) => Some(id.parse()?),
344                        None => None,
345                    };
346                    let label = ReleaseLabel {
347                        name: find_attr(e, b"name")?.to_string(),
348                        catno: find_attr_optional(e, b"catno")?.map(|c| c.to_string()),
349                        id,
350                    };
351                    self.current_item.labels.push(label);
352                    ParserState::Labels
353                }
354                _ => ParserState::Release,
355            },
356
357            ParserState::Series => match ev {
358                Event::Empty(e) => {
359                    let id = match find_attr_optional(e, b"id")? {
360                        Some(id) => Some(id.parse()?),
361                        None => None,
362                    };
363                    let series = ReleaseLabel {
364                        name: find_attr(e, b"name")?.to_string(),
365                        catno: find_attr_optional(e, b"catno")?.map(|c| c.to_string()),
366                        id,
367                    };
368                    self.current_item.series.push(series);
369                    ParserState::Series
370                }
371                _ => ParserState::Release,
372            },
373
374            ParserState::Videos => match ev {
375                Event::End(e) if e.local_name().as_ref() == b"videos" => ParserState::Release,
376
377                ev => {
378                    self.video_parser.process(ev)?;
379                    if self.video_parser.item_ready {
380                        self.current_item.videos.push(self.video_parser.take());
381                    }
382                    ParserState::Videos
383                }
384            },
385
386            ParserState::Images => match ev {
387                Event::Empty(e) if e.local_name().as_ref() == b"image" => {
388                    let image = Image::from_event(e)?;
389                    self.current_item.images.push(image);
390                    ParserState::Images
391                }
392                Event::End(e) if e.local_name().as_ref() == b"images" => ParserState::Release,
393
394                _ => ParserState::Images,
395            },
396
397            ParserState::TrackList => match ev {
398                Event::End(e) if e.local_name().as_ref() == b"tracklist" => ParserState::Release,
399
400                ev => {
401                    self.track_parser.process(ev)?;
402                    if self.track_parser.item_ready {
403                        self.current_item.tracklist.push(self.track_parser.take());
404                    }
405                    ParserState::TrackList
406                }
407            },
408
409            ParserState::Companies => match ev {
410                Event::End(e) if e.local_name().as_ref() == b"companies" => ParserState::Release,
411
412                ev => {
413                    self.company_parser.process(ev)?;
414                    if self.company_parser.item_ready {
415                        self.current_item.companies.push(self.company_parser.take());
416                    }
417                    ParserState::Companies
418                }
419            },
420        };
421
422        Ok(())
423    }
424}
425
426pub struct ReleaseBuilder {
427    inner: Release,
428}
429
430impl ReleaseBuilder {
431    pub fn id(mut self, id: u32) -> Self {
432        self.inner.id = id;
433        self
434    }
435
436    pub fn status(mut self, status: &str) -> Self {
437        self.inner.status = status.to_string();
438        self
439    }
440
441    pub fn title(mut self, title: &str) -> Self {
442        self.inner.title = title.to_string();
443        self
444    }
445
446    pub fn artist(mut self, credit: ArtistCredit) -> Self {
447        self.inner.artists.push(credit);
448        self
449    }
450
451    pub fn country(mut self, country: &str) -> Self {
452        self.inner.country = country.to_string();
453        self
454    }
455
456    pub fn label(mut self, id: Option<u32>, name: &str, catno: Option<&str>) -> Self {
457        self.inner.labels.push(ReleaseLabel {
458            id,
459            name: name.to_string(),
460            catno: catno.map(|c| c.to_string()),
461        });
462        self
463    }
464
465    pub fn series(mut self, id: Option<u32>, name: &str, catno: Option<&str>) -> Self {
466        self.inner.series.push(ReleaseLabel {
467            id,
468            name: name.to_string(),
469            catno: catno.map(|c| c.to_string()),
470        });
471        self
472    }
473
474    pub fn released(mut self, released: &str) -> Self {
475        self.inner.released = released.to_string();
476        self
477    }
478
479    pub fn notes(mut self, notes: &str) -> Self {
480        self.inner.notes = Some(notes.to_string());
481        self
482    }
483
484    pub fn genre(mut self, genre: &str) -> Self {
485        self.inner.genres.push(genre.to_string());
486        self
487    }
488
489    pub fn style(mut self, style: &str) -> Self {
490        self.inner.styles.push(style.to_string());
491        self
492    }
493
494    pub fn master_id(mut self, id: u32) -> Self {
495        self.inner.master_id = Some(id);
496        self
497    }
498
499    pub fn is_main_release(mut self, is: bool) -> Self {
500        self.inner.is_main_release = is;
501        self
502    }
503
504    pub fn data_quality(mut self, data_quality: &str) -> Self {
505        self.inner.data_quality = data_quality.to_string();
506        self
507    }
508
509    pub fn image(mut self, ty: &str, width: i16, height: i16) -> Self {
510        self.inner.images.push(Image {
511            r#type: ty.to_string(),
512            uri: None,
513            uri150: None,
514            width,
515            height,
516        });
517        self
518    }
519
520    pub fn video(mut self, src: &str, duration: u32, title: &str, description: &str) -> Self {
521        self.inner.videos.push(Video {
522            src: src.to_string(),
523            duration,
524            title: title.to_string(),
525            description: description.to_string(),
526            embed: true,
527        });
528        self
529    }
530
531    pub fn extraartist(mut self, builder: ArtistCreditBuilder) -> Self {
532        self.inner.extraartists.push(builder.build());
533        self
534    }
535
536    pub fn track(self, position: &str, title: &str) -> TrackBuilder {
537        TrackBuilder {
538            inner: Track {
539                position: position.to_string(),
540                title: title.to_string(),
541                ..Default::default()
542            },
543            release: self,
544        }
545    }
546
547    pub fn format(
548        mut self,
549        qty: &str,
550        name: &str,
551        text: Option<&str>,
552        descriptions: &[&'static str],
553    ) -> Self {
554        self.inner.formats.push(ReleaseFormat {
555            qty: qty.to_string(),
556            name: name.to_string(),
557            text: text.map(|t| t.to_string()),
558            descriptions: descriptions.iter().map(|d| d.to_string()).collect(),
559        });
560        self
561    }
562
563    pub fn company(
564        mut self,
565        id: u32,
566        name: &str,
567        catno: Option<&str>,
568        entity_type: u8,
569        entity_type_name: &str,
570    ) -> Self {
571        self.inner.companies.push(ReleaseCompany {
572            id: Some(id),
573            name: name.to_string(),
574            catno: catno.map(|c| c.to_string()),
575            entity_type,
576            entity_type_name: entity_type_name.to_string(),
577        });
578        self
579    }
580
581    pub fn identifier(mut self, ty: &str, description: Option<&str>, value: Option<&str>) -> Self {
582        self.inner.identifiers.push(ReleaseIdentifier {
583            r#type: ty.to_string(),
584            description: description.map(|d| d.to_string()),
585            value: value.map(|v| v.to_string()),
586        });
587        self
588    }
589
590    pub fn build(self) -> Release {
591        self.inner
592    }
593}
594
595pub struct TrackBuilder {
596    inner: Track,
597    release: ReleaseBuilder,
598}
599
600impl TrackBuilder {
601    pub fn duration(mut self, duration: &str) -> Self {
602        self.inner.duration = Some(duration.to_string());
603        self
604    }
605    pub fn artist(mut self, credit: ArtistCreditBuilder) -> Self {
606        self.inner.artists.push(credit.build());
607        self
608    }
609
610    pub fn extraartist(mut self, credit: ArtistCreditBuilder) -> Self {
611        self.inner.extraartists.push(credit.build());
612        self
613    }
614
615    pub fn build_track(mut self) -> ReleaseBuilder {
616        self.release.inner.tracklist.push(self.inner);
617        self.release
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use pretty_assertions::assert_eq;
624    use std::io::{BufRead, BufReader, Cursor};
625
626    use crate::artist_credit::{ArtistCredit, ArtistCreditBuilder};
627
628    use super::{Release, ReleasesReader};
629
630    fn parse(xml: &'static str) -> Release {
631        let reader: Box<dyn BufRead> = Box::new(BufReader::new(Cursor::new(xml)));
632        let mut reader = quick_xml::Reader::from_reader(reader);
633        reader.config_mut().trim_text(true);
634        ReleasesReader::new(reader, Vec::new()).next().unwrap()
635    }
636
637    fn credit(id: u32, name: &str) -> ArtistCreditBuilder {
638        ArtistCredit::builder(id, name)
639    }
640
641    #[test]
642    fn test_release_40299_20250501() {
643        let expected = Release::builder(40299, "New Beat - Take 4")
644            .artist(credit(194, "Various").build())
645            .country("Belgium")
646            .status("Accepted")
647            .label(Some(9789), "Subway Dance", Some("Subway Dance 4000"))
648            .label(Some(9789), "Subway Dance", Some("SD 4000-LP"))
649            .series(Some(183060), "Take", Some("4"))
650            .series(Some(475876), "A.B.-Sounds", None)
651            .released("1989")
652            .notes("Made in Belgium.")
653            .genre("Electronic")
654            .style("Acid")
655            .style("New Beat")
656            .master_id(35574)
657            .is_main_release(true)
658            .data_quality("Needs Vote")
659            .video("https://www.youtube.com/watch?v=Txq736EVa80", 181, "Tragic Error - Tanzen (1989)", "A Belgian New Beat classic!\r\n\r\nTrack produced and written by Patrick De Meyer.")
660            .video("https://www.youtube.com/watch?v=6KwqUVPJ-xc", 303, "Westbam-Monkey say monkey do", "Classic house from 1988,Label-Dance Trax,catalog#: DRX 612,format 12\" vinyl Germany 1988")
661            .extraartist(
662                credit(118541, "Maurice Engelen")
663                    .anv("The Maurice Engelen")
664                    .role("Compiled By"),
665            )
666            .extraartist(credit(501662, "Tejo De Roeck").role("Cover"))
667            .extraartist(credit(11701904, "Boy Toy (6)").role("Model"))
668            .extraartist(credit(3601091, "Annick Wets").role("Photography By [Photo]"))
669            .track("A1", "Tanzen")
670            .duration("3:37")
671            .artist(credit(7542, "Tragic Error"))
672            .extraartist(
673                credit(116415, "Patrick De Meyer")
674                    .anv("P. De Meyer")
675                    .role("Written-By"),
676            )
677            .build_track()
678            .track("A2", "New Beat, A Musical Phenomenon")
679            .duration("3:40")
680            .artist(credit(32087, "The Brotherhood Of Sleep"))
681            .extraartist(credit(221853, "Joey Morton").anv("Morton").role("Written-By"))
682            .extraartist(credit(25528, "Sherman").role("Written-By"))
683            .build_track()
684            .format("1", "Vinyl", None, &["LP", "Compilation"])
685            .company(216650, "BE's Songs", None, 21, "Published By")
686            .company(57563, "Music Man Import", None, 21, "Published By")
687            .identifier("Rights Society", None, Some("SABAM-BIEM"))
688            .identifier("Matrix / Runout", Some("Side A"), Some("SD 4000-A2"))
689            .identifier("Matrix / Runout", Some("Side B"), Some("SD 4000-B1 FOON"))
690            .build();
691
692        let parsed = parse(
693            r#"
694<release id="40299" status="Accepted">
695  <artists>
696    <artist>
697      <id>194</id>
698      <name>Various</name>
699    </artist>
700  </artists>
701  <title>New Beat - Take 4</title>
702  <labels>
703    <label name="Subway Dance" catno="Subway Dance 4000" id="9789"/>
704    <label name="Subway Dance" catno="SD 4000-LP" id="9789"/>
705  </labels>
706  <series>
707    <series name="Take" catno="4" id="183060"/>
708    <series name="A.B.-Sounds" catno="" id="475876"/>
709  </series>
710  <extraartists>
711    <artist>
712      <id>118541</id>
713      <name>Maurice Engelen</name>
714      <anv>The Maurice Engelen</anv>
715      <role>Compiled By</role>
716    </artist>
717    <artist>
718      <id>501662</id>
719      <name>Tejo De Roeck</name>
720      <role>Cover</role>
721    </artist>
722    <artist>
723      <id>11701904</id>
724      <name>Boy Toy (6)</name>
725      <role>Model</role>
726    </artist>
727    <artist>
728      <id>3601091</id>
729      <name>Annick Wets</name>
730      <role>Photography By [Photo]</role>
731    </artist>
732  </extraartists>
733  <formats>
734    <format name="Vinyl" qty="1" text="">
735      <descriptions>
736        <description>LP</description>
737        <description>Compilation</description>
738      </descriptions>
739    </format>
740  </formats>
741  <genres>
742    <genre>Electronic</genre>
743  </genres>
744  <styles>
745    <style>Acid</style>
746    <style>New Beat</style>
747  </styles>
748  <country>Belgium</country>
749  <released>1989</released>
750  <notes>Made in Belgium.</notes>
751  <data_quality>Needs Vote</data_quality>
752  <master_id is_main_release="true">35574</master_id>
753  <tracklist>
754    <track>
755      <position>A1</position>
756      <title>Tanzen</title>
757      <duration>3:37</duration>
758      <artists>
759        <artist>
760          <id>7542</id>
761          <name>Tragic Error</name>
762        </artist>
763      </artists>
764      <extraartists>
765        <artist>
766          <id>116415</id>
767          <name>Patrick De Meyer</name>
768          <anv>P. De Meyer</anv>
769          <role>Written-By</role>
770        </artist>
771      </extraartists>
772    </track>
773    <track>
774      <position>A2</position>
775      <title>New Beat, A Musical Phenomenon</title>
776      <duration>3:40</duration>
777      <artists>
778        <artist>
779          <id>32087</id>
780          <name>The Brotherhood Of Sleep</name>
781        </artist>
782      </artists>
783      <extraartists>
784        <artist>
785          <id>221853</id>
786          <name>Joey Morton</name>
787          <anv>Morton</anv>
788          <role>Written-By</role>
789        </artist>
790        <artist>
791          <id>25528</id>
792          <name>Sherman</name>
793          <role>Written-By</role>
794        </artist>
795      </extraartists>
796    </track>
797  </tracklist>
798  <identifiers>
799    <identifier type="Rights Society" description="" value="SABAM-BIEM"/>
800    <identifier type="Matrix / Runout" description="Side A" value="SD 4000-A2"/>
801    <identifier type="Matrix / Runout" description="Side B" value="SD 4000-B1 FOON"/>
802  </identifiers>
803  <videos>
804    <video src="https://www.youtube.com/watch?v=Txq736EVa80" duration="181" embed="true">
805      <title>Tragic Error - Tanzen (1989)</title>
806      <description>A Belgian New Beat classic!&#13;
807&#13;
808Track produced and written by Patrick De Meyer.</description>
809    </video>
810    <video src="https://www.youtube.com/watch?v=6KwqUVPJ-xc" duration="303" embed="true">
811      <title>Westbam-Monkey say monkey do</title>
812      <description>Classic house from 1988,Label-Dance Trax,catalog#: DRX 612,format 12" vinyl Germany 1988</description>
813    </video>
814  </videos>
815  <companies>
816    <company>
817      <id>216650</id>
818      <name>BE's Songs</name>
819      <entity_type>21</entity_type>
820      <entity_type_name>Published By</entity_type_name>
821      <resource_url>https://api.discogs.com/labels/216650</resource_url>
822    </company>
823    <company>
824      <id>57563</id>
825      <name>Music Man Import</name>
826      <entity_type>21</entity_type>
827      <entity_type_name>Published By</entity_type_name>
828      <resource_url>https://api.discogs.com/labels/57563</resource_url>
829    </company>
830  </companies>
831</release>
832        "#,
833        );
834        assert_eq!(expected, parsed)
835    }
836
837    #[test]
838    fn test_release_40299_20231001() {
839        let expected = Release::builder(40299, "New Beat - Take 4")
840            .artist(credit(194, "Various").build())
841            .country("Belgium")
842            .status("Accepted")
843            .label(Some(9789), "Subway Dance", Some("Subway Dance 4000"))
844            .label(Some(9789), "Subway Dance", Some("SD 4000-LP"))
845            .released("1989")
846            .notes("Made in Belgium.")
847            .genre("Electronic")
848            .style("Acid")
849            .style("New Beat")
850            .master_id(35574)
851            .is_main_release(true)
852            .data_quality("Needs Vote")
853            .video("https://www.youtube.com/watch?v=Txq736EVa80", 181, "Tragic Error - Tanzen (1989)", "A Belgian New Beat classic!\r\n\r\nTrack produced and written by Patrick De Meyer.")
854            .extraartist(
855                credit(118541, "Maurice Engelen")
856                    .anv("The Maurice Engelen")
857                    .role("Compiled By"),
858            )
859            .extraartist(credit(501662, "Tejo De Roeck").role("Cover"))
860            .extraartist(credit(11701904, "Boy Toy (6)").role("Model"))
861            .extraartist(credit(3601091, "Annick Wets").role("Photography By [Photo]"))
862            .track("A1", "Tanzen")
863            .duration("3:37")
864            .artist(credit(7542, "Tragic Error"))
865            .extraartist(
866                credit(116415, "Patrick De Meyer")
867                    .anv("P. De Meyer")
868                    .role("Written-By"),
869            )
870            .build_track()
871            .track("A2", "New Beat, A Musical Phenomenon")
872            .duration("3:40")
873            .artist(credit(32087, "The Brotherhood Of Sleep"))
874            .extraartist(credit(221853, "Joey Morton").anv("Morton").role("Written-By"))
875            .extraartist(credit(25528, "Sherman").role("Written-By"))
876            .build_track()
877            .format("1", "Vinyl", None, &["LP", "Compilation"])
878            .company(216650, "BE's Songs", None, 21, "Published By")
879            .company(57563, "Music Man Import", None, 21, "Published By")
880            .identifier("Rights Society", None, Some("SABAM-BIEM"))
881            .identifier("Matrix / Runout", Some("Side A"), Some("SD 4000-A2"))
882            .identifier("Matrix / Runout", Some("Side B"), Some("SD 4000-B1 FOON"))
883            .image("primary", 600, 595)
884            .image("secondary", 600, 614)
885            .image("secondary", 589, 600)
886            .build();
887
888        let parsed = parse(
889            r#"
890<release id="40299" status="Accepted">
891  <images>
892    <image type="primary" uri="" uri150="" width="600" height="595"/>
893    <image type="secondary" uri="" uri150="" width="600" height="614"/>
894    <image type="secondary" uri="" uri150="" width="589" height="600"/>
895  </images>
896  <artists>
897    <artist>
898      <id>194</id>
899      <name>Various</name>
900      <anv>
901      </anv>
902      <join>
903      </join>
904      <role>
905      </role>
906      <tracks>
907      </tracks>
908    </artist>
909  </artists>
910  <title>New Beat - Take 4</title>
911  <labels>
912    <label name="Subway Dance" catno="Subway Dance 4000" id="9789"/>
913    <label name="Subway Dance" catno="SD 4000-LP" id="9789"/>
914  </labels>
915  <extraartists>
916    <artist>
917      <id>118541</id>
918      <name>Maurice Engelen</name>
919      <anv>The Maurice Engelen</anv>
920      <join>
921      </join>
922      <role>Compiled By</role>
923      <tracks>
924      </tracks>
925    </artist>
926    <artist>
927      <id>501662</id>
928      <name>Tejo De Roeck</name>
929      <anv>
930      </anv>
931      <join>
932      </join>
933      <role>Cover</role>
934      <tracks>
935      </tracks>
936    </artist>
937    <artist>
938      <id>11701904</id>
939      <name>Boy Toy (6)</name>
940      <anv>
941      </anv>
942      <join>
943      </join>
944      <role>Model</role>
945      <tracks>
946      </tracks>
947    </artist>
948    <artist>
949      <id>3601091</id>
950      <name>Annick Wets</name>
951      <anv>
952      </anv>
953      <join>
954      </join>
955      <role>Photography By [Photo]</role>
956      <tracks>
957      </tracks>
958    </artist>
959  </extraartists>
960  <formats>
961    <format name="Vinyl" qty="1" text="">
962      <descriptions>
963        <description>LP</description>
964        <description>Compilation</description>
965      </descriptions>
966    </format>
967  </formats>
968  <genres>
969    <genre>Electronic</genre>
970  </genres>
971  <styles>
972    <style>Acid</style>
973    <style>New Beat</style>
974  </styles>
975  <country>Belgium</country>
976  <released>1989</released>
977  <notes>Made in Belgium.</notes>
978  <data_quality>Needs Vote</data_quality>
979  <master_id is_main_release="true">35574</master_id>
980  <tracklist>
981    <track>
982      <position>A1</position>
983      <title>Tanzen</title>
984      <duration>3:37</duration>
985      <artists>
986        <artist>
987          <id>7542</id>
988          <name>Tragic Error</name>
989          <anv>
990          </anv>
991          <join>
992          </join>
993          <role>
994          </role>
995          <tracks>
996          </tracks>
997        </artist>
998      </artists>
999      <extraartists>
1000        <artist>
1001          <id>116415</id>
1002          <name>Patrick De Meyer</name>
1003          <anv>P. De Meyer</anv>
1004          <join>
1005          </join>
1006          <role>Written-By</role>
1007          <tracks>
1008          </tracks>
1009        </artist>
1010      </extraartists>
1011    </track>
1012    <track>
1013      <position>A2</position>
1014      <title>New Beat, A Musical Phenomenon</title>
1015      <duration>3:40</duration>
1016      <artists>
1017        <artist>
1018          <id>32087</id>
1019          <name>The Brotherhood Of Sleep</name>
1020          <anv>
1021          </anv>
1022          <join>
1023          </join>
1024          <role>
1025          </role>
1026          <tracks>
1027          </tracks>
1028        </artist>
1029      </artists>
1030      <extraartists>
1031        <artist>
1032          <id>221853</id>
1033          <name>Joey Morton</name>
1034          <anv>Morton</anv>
1035          <join>
1036          </join>
1037          <role>Written-By</role>
1038          <tracks>
1039          </tracks>
1040        </artist>
1041        <artist>
1042          <id>25528</id>
1043          <name>Sherman</name>
1044          <anv>
1045          </anv>
1046          <join>
1047          </join>
1048          <role>Written-By</role>
1049          <tracks>
1050          </tracks>
1051        </artist>
1052      </extraartists>
1053    </track>
1054  </tracklist>
1055  <identifiers>
1056    <identifier type="Rights Society" value="SABAM-BIEM"/>
1057    <identifier type="Matrix / Runout" description="Side A" value="SD 4000-A2"/>
1058    <identifier type="Matrix / Runout" description="Side B" value="SD 4000-B1 FOON"/>
1059  </identifiers>
1060  <videos>
1061    <video src="https://www.youtube.com/watch?v=Txq736EVa80" duration="181" embed="true">
1062      <title>Tragic Error - Tanzen (1989)</title>
1063      <description>A Belgian New Beat classic!&#13;
1064&#13;
1065Track produced and written by Patrick De Meyer.</description>
1066    </video>
1067  </videos>
1068  <companies>
1069    <company>
1070      <id>216650</id>
1071      <name>BE's Songs</name>
1072      <catno>
1073      </catno>
1074      <entity_type>21</entity_type>
1075      <entity_type_name>Published By</entity_type_name>
1076      <resource_url>https://api.discogs.com/labels/216650</resource_url>
1077    </company>
1078    <company>
1079      <id>57563</id>
1080      <name>Music Man Import</name>
1081      <catno>
1082      </catno>
1083      <entity_type>21</entity_type>
1084      <entity_type_name>Published By</entity_type_name>
1085      <resource_url>https://api.discogs.com/labels/57563</resource_url>
1086    </company>
1087  </companies>
1088</release>
1089        "#,
1090        );
1091        assert_eq!(expected, parsed)
1092    }
1093}