disco_quick/
artist.rs

1use crate::parser::{Parser, ParserError};
2use crate::reader::XmlReader;
3use crate::shared::Image;
4use crate::util::{find_attr, maybe_text};
5use log::debug;
6use quick_xml::events::Event;
7use std::fmt;
8use std::mem::take;
9
10#[derive(Clone, Debug, Default, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct Artist {
13    pub id: u32,
14    pub name: String,
15    pub real_name: Option<String>,
16    pub profile: Option<String>,
17    pub data_quality: String,
18    pub name_variations: Vec<String>,
19    pub urls: Vec<String>,
20    pub aliases: Vec<ArtistInfo>,
21    pub members: Vec<ArtistInfo>,
22    pub groups: Vec<ArtistInfo>,
23    pub images: Vec<Image>,
24}
25
26impl Artist {
27    pub fn builder(id: u32, name: &str) -> ArtistBuilder {
28        ArtistBuilder {
29            inner: Artist {
30                id,
31                name: name.to_string(),
32                ..Default::default()
33            },
34        }
35    }
36}
37
38#[derive(Clone, Debug, Default, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct ArtistInfo {
41    pub id: u32,
42    pub name: String,
43}
44
45impl fmt::Display for Artist {
46    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47        write!(f, "{}", self.name)
48    }
49}
50
51pub struct ArtistsReader {
52    buf: Vec<u8>,
53    reader: XmlReader,
54    parser: ArtistParser,
55}
56
57impl ArtistsReader {
58    pub fn new(reader: XmlReader, buf: Vec<u8>) -> Self {
59        Self {
60            buf,
61            reader,
62            parser: ArtistParser::new(),
63        }
64    }
65}
66
67impl Iterator for ArtistsReader {
68    type Item = Artist;
69    fn next(&mut self) -> Option<Self::Item> {
70        loop {
71            match self.reader.read_event_into(&mut self.buf).unwrap() {
72                Event::Eof => {
73                    return None;
74                }
75                ev => self.parser.process(&ev).unwrap(),
76            };
77            if self.parser.item_ready {
78                return Some(self.parser.take());
79            }
80            self.buf.clear();
81        }
82    }
83}
84
85#[derive(Debug, Default)]
86enum ParserState {
87    #[default]
88    Artist,
89    Id,
90    Name,
91    RealName,
92    Profile,
93    DataQuality,
94    NameVariations,
95    Urls,
96    Aliases,
97    Members,
98    MemberId,
99    MemberName,
100    Groups,
101    Images,
102}
103
104#[derive(Debug, Default)]
105pub struct ArtistParser {
106    state: ParserState,
107    current_item: Artist,
108    item_ready: bool,
109}
110
111impl Parser for ArtistParser {
112    type Item = Artist;
113    fn new() -> Self {
114        Self::default()
115    }
116
117    fn take(&mut self) -> Self::Item {
118        self.item_ready = false;
119        take(&mut self.current_item)
120    }
121
122    fn process(&mut self, ev: &Event) -> Result<(), ParserError> {
123        self.state = match self.state {
124            ParserState::Artist => match ev {
125                Event::Start(e) if e.local_name().as_ref() == b"artist" => ParserState::Artist,
126
127                Event::Start(e) => match e.local_name().as_ref() {
128                    b"id" => ParserState::Id,
129                    b"name" => ParserState::Name,
130                    b"realname" => ParserState::RealName,
131                    b"profile" => ParserState::Profile,
132                    b"data_quality" => ParserState::DataQuality,
133                    b"urls" => ParserState::Urls,
134                    b"namevariations" => ParserState::NameVariations,
135                    b"aliases" => ParserState::Aliases,
136                    b"members" => ParserState::Members,
137                    b"groups" => ParserState::Groups,
138                    b"images" => ParserState::Images,
139                    _ => ParserState::Artist,
140                },
141                Event::End(e) if e.local_name().as_ref() == b"artist" => {
142                    self.item_ready = true;
143                    ParserState::Artist
144                }
145                Event::End(e) if e.local_name().as_ref() == b"artists" => ParserState::Artist,
146
147                _ => ParserState::Artist,
148            },
149
150            ParserState::Id => match ev {
151                Event::Text(e) => {
152                    self.current_item.id = e.unescape()?.parse()?;
153                    debug!("Began parsing Artist {}", self.current_item.id);
154                    ParserState::Id
155                }
156                _ => ParserState::Artist,
157            },
158
159            ParserState::Name => match ev {
160                Event::Text(e) => {
161                    self.current_item.name = e.unescape()?.to_string();
162                    ParserState::Name
163                }
164                _ => ParserState::Artist,
165            },
166
167            ParserState::RealName => match ev {
168                Event::Text(e) => {
169                    self.current_item.real_name = maybe_text(e)?;
170                    ParserState::RealName
171                }
172                _ => ParserState::Artist,
173            },
174
175            ParserState::Profile => match ev {
176                Event::Text(e) => {
177                    self.current_item.profile = maybe_text(e)?;
178                    ParserState::Profile
179                }
180                _ => ParserState::Artist,
181            },
182
183            ParserState::DataQuality => match ev {
184                Event::Text(e) => {
185                    self.current_item.data_quality = e.unescape()?.to_string();
186                    ParserState::DataQuality
187                }
188                _ => ParserState::Artist,
189            },
190
191            ParserState::Urls => match ev {
192                Event::End(e) if e.local_name().as_ref() == b"urls" => ParserState::Artist,
193
194                Event::Text(e) => {
195                    self.current_item.urls.push(e.unescape()?.to_string());
196                    ParserState::Urls
197                }
198                _ => ParserState::Urls,
199            },
200
201            ParserState::Aliases => match ev {
202                Event::Start(e) if e.local_name().as_ref() == b"name" => {
203                    let alias = ArtistInfo {
204                        id: find_attr(e, b"id")?.parse()?,
205                        ..Default::default()
206                    };
207                    self.current_item.aliases.push(alias);
208                    ParserState::Aliases
209                }
210                Event::Text(e) => {
211                    let Some(alias) = self.current_item.aliases.last_mut() else {
212                        return Err(ParserError::MissingData("Artist alias ID"));
213                    };
214                    alias.name = e.unescape()?.to_string();
215                    ParserState::Aliases
216                }
217                Event::End(e) if e.local_name().as_ref() == b"aliases" => ParserState::Artist,
218
219                _ => ParserState::Aliases,
220            },
221
222            ParserState::Members => match ev {
223                Event::Start(e) if e.local_name().as_ref() == b"name" => {
224                    let member = ArtistInfo {
225                        id: find_attr(e, b"id")?.parse()?,
226                        ..Default::default()
227                    };
228                    self.current_item.members.push(member);
229                    ParserState::MemberName
230                }
231                Event::Start(e) if e.local_name().as_ref() == b"id" => ParserState::MemberId,
232                Event::End(e) if e.local_name().as_ref() == b"members" => ParserState::Artist,
233                _ => ParserState::Members,
234            },
235
236            // Removed from the dumps in 2025, but remains present as an attr of the member name
237            ParserState::MemberId => match ev {
238                Event::Text(_) => ParserState::MemberId,
239                _ => ParserState::Members,
240            },
241
242            ParserState::MemberName => match ev {
243                Event::Text(e) => {
244                    let Some(member) = self.current_item.members.last_mut() else {
245                        return Err(ParserError::MissingData("Artist member ID"));
246                    };
247                    member.name = e.unescape()?.to_string();
248                    ParserState::Members
249                }
250                _ => ParserState::Members,
251            },
252
253            ParserState::Groups => match ev {
254                Event::Start(e) if e.local_name().as_ref() == b"name" => {
255                    let group = ArtistInfo {
256                        id: find_attr(e, b"id")?.parse()?,
257                        ..Default::default()
258                    };
259                    self.current_item.groups.push(group);
260                    ParserState::Groups
261                }
262                Event::Text(e) => {
263                    let Some(group) = self.current_item.groups.last_mut() else {
264                        return Err(ParserError::MissingData("Artist group ID"));
265                    };
266                    group.name = e.unescape()?.to_string();
267                    ParserState::Groups
268                }
269                Event::End(e) if e.local_name().as_ref() == b"groups" => ParserState::Artist,
270
271                _ => ParserState::Groups,
272            },
273
274            ParserState::NameVariations => match ev {
275                Event::Text(e) => {
276                    let anv = e.unescape()?.to_string();
277                    self.current_item.name_variations.push(anv);
278                    ParserState::NameVariations
279                }
280                Event::End(e) if e.local_name().as_ref() == b"namevariations" => {
281                    ParserState::Artist
282                }
283                _ => ParserState::NameVariations,
284            },
285
286            ParserState::Images => match ev {
287                Event::Empty(e) if e.local_name().as_ref() == b"image" => {
288                    let image = Image::from_event(e)?;
289                    self.current_item.images.push(image);
290                    ParserState::Images
291                }
292                Event::End(e) if e.local_name().as_ref() == b"images" => ParserState::Artist,
293
294                _ => ParserState::Images,
295            },
296        };
297
298        Ok(())
299    }
300}
301
302pub struct ArtistBuilder {
303    inner: Artist,
304}
305
306impl ArtistBuilder {
307    pub fn id(mut self, id: u32) -> Self {
308        self.inner.id = id;
309        self
310    }
311
312    pub fn name(mut self, name: &str) -> Self {
313        self.inner.name = name.to_string();
314        self
315    }
316
317    pub fn real_name(mut self, real_name: &str) -> Self {
318        self.inner.real_name = Some(real_name.to_string());
319        self
320    }
321
322    pub fn profile(mut self, profile: &str) -> Self {
323        self.inner.profile = Some(profile.to_string());
324        self
325    }
326
327    pub fn data_quality(mut self, data_quality: &str) -> Self {
328        self.inner.data_quality = data_quality.to_string();
329        self
330    }
331
332    pub fn name_variation(mut self, name_variation: &str) -> Self {
333        self.inner.name_variations.push(name_variation.to_owned());
334        self
335    }
336
337    pub fn url(mut self, url: &str) -> Self {
338        self.inner.urls.push(url.to_string());
339        self
340    }
341
342    pub fn alias(mut self, id: u32, name: &str) -> Self {
343        self.inner.aliases.push(ArtistInfo {
344            id,
345            name: name.to_string(),
346        });
347        self
348    }
349
350    pub fn member(mut self, id: u32, name: &str) -> Self {
351        self.inner.members.push(ArtistInfo {
352            id,
353            name: name.to_string(),
354        });
355        self
356    }
357
358    pub fn group(mut self, id: u32, name: &str) -> Self {
359        self.inner.groups.push(ArtistInfo {
360            id,
361            name: name.to_string(),
362        });
363        self
364    }
365
366    pub fn image(mut self, ty: &str, width: i16, height: i16) -> Self {
367        self.inner.images.push(Image {
368            r#type: ty.to_string(),
369            uri: None,
370            uri150: None,
371            width,
372            height,
373        });
374        self
375    }
376
377    pub fn build(self) -> Artist {
378        self.inner
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use pretty_assertions::assert_eq;
385    use std::io::{BufRead, BufReader, Cursor};
386
387    use super::{Artist, ArtistsReader};
388
389    fn parse(xml: &'static str) -> Artist {
390        let reader: Box<dyn BufRead> = Box::new(BufReader::new(Cursor::new(xml)));
391        let mut reader = quick_xml::Reader::from_reader(reader);
392        reader.config_mut().trim_text(true);
393        ArtistsReader::new(reader, Vec::new()).next().unwrap()
394    }
395
396    #[test]
397    fn test_artist_2_20231001() {
398        let expected = Artist::builder(2, "Mr. James Barth & A.D.")
399            .real_name("Cari Lekebusch & Alexi Delano")
400            .data_quality("Correct")
401            .name_variation("MR JAMES BARTH & A. D.")
402            .name_variation("Mr Barth & A.D.")
403            .name_variation("Mr. Barth & A.D.")
404            .name_variation("Mr. James Barth & A. D.")
405            .alias(2470, "Puente Latino")
406            .alias(19536, "Yakari & Delano")
407            .alias(103709, "Crushed Insect & The Sick Puppy")
408            .alias(384581, "ADCL")
409            .alias(1779857, "Alexi Delano & Cari Lekebusch")
410            .member(26, "Alexi Delano")
411            .member(27, "Cari Lekebusch")
412            .build();
413        let parsed = parse(
414            r#"
415<artist>
416  <id>2</id>
417  <name>Mr. James Barth &amp; A.D.</name>
418  <realname>Cari Lekebusch &amp; Alexi Delano</realname>
419  <profile>
420  </profile>
421  <data_quality>Correct</data_quality>
422  <namevariations>
423    <name>MR JAMES BARTH &amp; A. D.</name>
424    <name>Mr Barth &amp; A.D.</name>
425    <name>Mr. Barth &amp; A.D.</name>
426    <name>Mr. James Barth &amp; A. D.</name>
427  </namevariations>
428  <aliases>
429    <name id="2470">Puente Latino</name>
430    <name id="19536">Yakari &amp; Delano</name>
431    <name id="103709">Crushed Insect &amp; The Sick Puppy</name>
432    <name id="384581">ADCL</name>
433    <name id="1779857">Alexi Delano &amp; Cari Lekebusch</name>
434  </aliases>
435  <members>
436    <id>26</id>
437    <name id="26">Alexi Delano</name>
438    <id>27</id>
439    <name id="27">Cari Lekebusch</name>
440  </members>
441</artist>"#,
442        );
443        assert_eq!(expected, parsed);
444    }
445    #[test]
446    fn test_artist_2_20250501() {
447        let expected = Artist::builder(2, "Mr. James Barth & A.D.")
448            .data_quality("Correct")
449            .name_variation("MR JAMES BARTH & A. D.")
450            .name_variation("Mr Barth & A.D.")
451            .name_variation("Mr. Barth & A.D.")
452            .name_variation("Mr. James Barth & A. D.")
453            .alias(2470, "Puente Latino")
454            .alias(19536, "Yakari & Delano")
455            .alias(103709, "Crushed Insect & The Sick Puppy")
456            .alias(384581, "ADCL")
457            .alias(1779857, "Alexi Delano & Cari Lekebusch")
458            .member(26, "Alexi Delano")
459            .member(27, "Cari Lekebusch")
460            .build();
461        let parsed = parse(
462            r#"
463<artist>
464  <id>2</id>
465  <name>Mr. James Barth &amp; A.D.</name>
466  <data_quality>Correct</data_quality>
467  <namevariations>
468    <name>MR JAMES BARTH &amp; A. D.</name>
469    <name>Mr Barth &amp; A.D.</name>
470    <name>Mr. Barth &amp; A.D.</name>
471    <name>Mr. James Barth &amp; A. D.</name>
472  </namevariations>
473  <aliases>
474    <name id="2470">Puente Latino</name>
475    <name id="19536">Yakari &amp; Delano</name>
476    <name id="103709">Crushed Insect &amp; The Sick Puppy</name>
477    <name id="384581">ADCL</name>
478    <name id="1779857">Alexi Delano &amp; Cari Lekebusch</name>
479  </aliases>
480  <members>
481    <name id="26">Alexi Delano</name>
482    <name id="27">Cari Lekebusch</name>
483  </members>
484</artist>"#,
485        );
486        assert_eq!(expected, parsed);
487    }
488
489    #[test]
490    fn test_artist_26_20231001() {
491        let expected = Artist::builder(26, "Alexi Delano")
492            .profile("Alexi Delano ‘s music production dwells in perfect balance between shiny minimalism and dark vivacious techno. With more than two decades on stage he has been able to combine different roots and facets of the contemporary music scene.\r\nBorn in Chile, raised in Sweden and later on adopted by New York City, Alexi was part of the Swedish wave of electronic music producers of the mid 90’s such as Adam Beyer, Cari Lekebusch, Jesper Dahlback and Joel Mull. Moving from Scandinavia to New York influenced him to combine the heavy compressed Swedish sound with the vibrancy of the creative music scene of Brooklyn.\r\n\r\nThroughout his music career, Alexi has been nominated for the Swedish Music Award ‘P3 Guld’ (an alternative to the Swedish Grammy), produced six albums and released countless records on established labels such as the iconic Swedish label SVEK, Plus 8, Minus, Hybrid, Drumcode, Visionquest, Spectral Sound, Get Physical, Poker Flat and many more. \r\nWith a music production and DJ style swinging between house and techno, he is consistently reinventing himself with each new release.")
493            .data_quality("Needs Vote")
494            .name_variation("A Delano")
495            .name_variation("A. D.")
496            .url("https://www.facebook.com/alexidelanomusic")
497            .url("http://www.soundcloud.com/alexidelano")
498            .url("http://twitter.com/AlexiDelano")
499            .alias(50, "ADNY")
500            .alias(937, "G.O.L.")
501            .group(2, "Mr. James Barth & A.D.")
502            .group(254, "ADNY & The Persuader")
503            .image("primary", 600, 269)
504            .image("secondary", 600, 400)
505            .build();
506        let parsed = parse(
507            r#"
508<artist>
509  <images>
510    <image type="primary" uri="" uri150="" width="600" height="269"/>
511    <image type="secondary" uri="" uri150="" width="600" height="400"/>
512  </images>
513  <id>26</id>
514  <name>Alexi Delano</name>
515  <profile>Alexi Delano ‘s music production dwells in perfect balance between shiny minimalism and dark vivacious techno. With more than two decades on stage he has been able to combine different roots and facets of the contemporary music scene.&#13;
516Born in Chile, raised in Sweden and later on adopted by New York City, Alexi was part of the Swedish wave of electronic music producers of the mid 90’s such as Adam Beyer, Cari Lekebusch, Jesper Dahlback and Joel Mull. Moving from Scandinavia to New York influenced him to combine the heavy compressed Swedish sound with the vibrancy of the creative music scene of Brooklyn.&#13;
517&#13;
518Throughout his music career, Alexi has been nominated for the Swedish Music Award ‘P3 Guld’ (an alternative to the Swedish Grammy), produced six albums and released countless records on established labels such as the iconic Swedish label SVEK, Plus 8, Minus, Hybrid, Drumcode, Visionquest, Spectral Sound, Get Physical, Poker Flat and many more. &#13;
519With a music production and DJ style swinging between house and techno, he is consistently reinventing himself with each new release.</profile>
520  <data_quality>Needs Vote</data_quality>
521  <urls>
522    <url>https://www.facebook.com/alexidelanomusic</url>
523    <url>http://www.soundcloud.com/alexidelano</url>
524    <url>http://twitter.com/AlexiDelano</url>
525  </urls>
526  <namevariations>
527    <name>A Delano</name>
528    <name>A. D.</name>
529  </namevariations>
530  <aliases>
531    <name id="50">ADNY</name>
532    <name id="937">G.O.L.</name>
533  </aliases>
534  <groups>
535    <name id="2">Mr. James Barth &amp; A.D.</name>
536    <name id="254">ADNY &amp; The Persuader</name>
537  </groups>
538</artist>"#,
539        );
540        assert_eq!(expected, parsed);
541    }
542}