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 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 & A.D.</name>
418 <realname>Cari Lekebusch & Alexi Delano</realname>
419 <profile>
420 </profile>
421 <data_quality>Correct</data_quality>
422 <namevariations>
423 <name>MR JAMES BARTH & A. D.</name>
424 <name>Mr Barth & A.D.</name>
425 <name>Mr. Barth & A.D.</name>
426 <name>Mr. James Barth & A. D.</name>
427 </namevariations>
428 <aliases>
429 <name id="2470">Puente Latino</name>
430 <name id="19536">Yakari & Delano</name>
431 <name id="103709">Crushed Insect & The Sick Puppy</name>
432 <name id="384581">ADCL</name>
433 <name id="1779857">Alexi Delano & 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 & A.D.</name>
466 <data_quality>Correct</data_quality>
467 <namevariations>
468 <name>MR JAMES BARTH & A. D.</name>
469 <name>Mr Barth & A.D.</name>
470 <name>Mr. Barth & A.D.</name>
471 <name>Mr. James Barth & A. D.</name>
472 </namevariations>
473 <aliases>
474 <name id="2470">Puente Latino</name>
475 <name id="19536">Yakari & Delano</name>
476 <name id="103709">Crushed Insect & The Sick Puppy</name>
477 <name id="384581">ADCL</name>
478 <name id="1779857">Alexi Delano & 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.
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.
517
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.
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 & A.D.</name>
536 <name id="254">ADNY & The Persuader</name>
537 </groups>
538</artist>"#,
539 );
540 assert_eq!(expected, parsed);
541 }
542}