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, 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!
807
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!
1064
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}