1use crate::artist_credit::{
2 get_credit_string, ArtistCredit, ArtistCreditBuilder, ArtistCreditParser,
3};
4use crate::parser::{Parser, ParserError};
5use crate::reader::XmlReader;
6use crate::shared::Image;
7use crate::util::{find_attr, maybe_text};
8use crate::video::{Video, VideoParser};
9use log::debug;
10use quick_xml::events::Event;
11use std::fmt;
12use std::mem::take;
13
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Master {
17 pub id: u32,
18 pub title: String,
19 pub main_release: u32,
20 pub year: u16,
21 pub notes: Option<String>,
22 pub genres: Vec<String>,
23 pub styles: Vec<String>,
24 pub data_quality: String,
25 pub artists: Vec<ArtistCredit>,
26 pub images: Vec<Image>,
27 pub videos: Vec<Video>,
28}
29impl Master {
30 pub fn builder(id: u32, title: &str) -> MasterBuilder {
31 MasterBuilder {
32 inner: Master {
33 id,
34 title: title.to_string(),
35 ..Default::default()
36 },
37 }
38 }
39}
40
41impl fmt::Display for Master {
42 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43 let artist_credit = get_credit_string(&self.artists);
44 write!(f, "{} - {}", artist_credit, self.title)
45 }
46}
47
48pub struct MastersReader {
49 buf: Vec<u8>,
50 reader: XmlReader,
51 parser: MasterParser,
52}
53
54impl MastersReader {
55 pub fn new(reader: XmlReader, buf: Vec<u8>) -> Self {
56 Self {
57 buf,
58 reader,
59 parser: MasterParser::new(),
60 }
61 }
62}
63
64impl Iterator for MastersReader {
65 type Item = Master;
66 fn next(&mut self) -> Option<Self::Item> {
67 loop {
68 match self.reader.read_event_into(&mut self.buf).unwrap() {
69 Event::Eof => {
70 return None;
71 }
72 ev => self.parser.process(&ev).unwrap(),
73 };
74 if self.parser.item_ready {
75 return Some(self.parser.take());
76 }
77 self.buf.clear();
78 }
79 }
80}
81
82#[derive(Debug, Default)]
83enum ParserState {
84 #[default]
85 Master,
86 MainRelease,
87 Artists,
88 Title,
89 DataQuality,
90 Notes,
91 Images,
92 Styles,
93 Genres,
94 Year,
95 Videos,
96}
97
98#[derive(Debug, Default)]
99pub struct MasterParser {
100 state: ParserState,
101 current_item: Master,
102 artist_parser: ArtistCreditParser,
103 videos_parser: VideoParser,
104 item_ready: bool,
105}
106
107impl Parser for MasterParser {
108 type Item = Master;
109 fn new() -> Self {
110 Self::default()
111 }
112
113 fn take(&mut self) -> Master {
114 self.item_ready = false;
115 take(&mut self.current_item)
116 }
117
118 fn process(&mut self, ev: &Event) -> Result<(), ParserError> {
119 self.state = match self.state {
120 ParserState::Master => match ev {
121 Event::Start(e) if e.local_name().as_ref() == b"master" => {
122 self.current_item.id = find_attr(e, b"id")?.parse()?;
123 debug!("Began parsing Master {}", self.current_item.id);
124 ParserState::Master
125 }
126
127 Event::Start(e) => match e.local_name().as_ref() {
128 b"main_release" => ParserState::MainRelease,
129 b"title" => ParserState::Title,
130 b"artists" => ParserState::Artists,
131 b"data_quality" => ParserState::DataQuality,
132 b"images" => ParserState::Images,
133 b"styles" => ParserState::Styles,
134 b"genres" => ParserState::Genres,
135 b"notes" => ParserState::Notes,
136 b"year" => ParserState::Year,
137 b"videos" => ParserState::Videos,
138 _ => ParserState::Master,
139 },
140
141 Event::End(e) if e.local_name().as_ref() == b"master" => {
142 self.item_ready = true;
143 ParserState::Master
144 }
145
146 _ => ParserState::Master,
147 },
148
149 ParserState::MainRelease => match ev {
150 Event::Text(e) => {
151 self.current_item.main_release = e.unescape()?.parse()?;
152 ParserState::MainRelease
153 }
154 _ => ParserState::Master,
155 },
156
157 ParserState::Artists => match ev {
158 Event::End(e) if e.local_name().as_ref() == b"artists" => ParserState::Master,
159
160 ev => {
161 self.artist_parser.process(ev)?;
162 if self.artist_parser.item_ready {
163 self.current_item.artists.push(self.artist_parser.take());
164 }
165 ParserState::Artists
166 }
167 },
168
169 ParserState::Title => match ev {
170 Event::Text(e) => {
171 self.current_item.title = e.unescape()?.to_string();
172 ParserState::Title
173 }
174 _ => ParserState::Master,
175 },
176
177 ParserState::DataQuality => match ev {
178 Event::Text(e) => {
179 self.current_item.data_quality = e.unescape()?.to_string();
180 ParserState::DataQuality
181 }
182 _ => ParserState::Master,
183 },
184
185 ParserState::Images => match ev {
186 Event::Empty(e) if e.local_name().as_ref() == b"image" => {
187 let image = Image::from_event(e)?;
188 self.current_item.images.push(image);
189 ParserState::Images
190 }
191 Event::End(e) if e.local_name().as_ref() == b"images" => ParserState::Master,
192
193 _ => ParserState::Images,
194 },
195
196 ParserState::Genres => match ev {
197 Event::End(e) if e.local_name().as_ref() == b"genres" => ParserState::Master,
198
199 Event::Text(e) => {
200 self.current_item.genres.push(e.unescape()?.to_string());
201 ParserState::Genres
202 }
203 _ => ParserState::Genres,
204 },
205
206 ParserState::Styles => match ev {
207 Event::End(e) if e.local_name().as_ref() == b"styles" => ParserState::Master,
208
209 Event::Text(e) => {
210 self.current_item.styles.push(e.unescape()?.to_string());
211 ParserState::Styles
212 }
213 _ => ParserState::Styles,
214 },
215
216 ParserState::Notes => match ev {
217 Event::Text(e) => {
218 self.current_item.notes = maybe_text(e)?;
219 ParserState::Notes
220 }
221 _ => ParserState::Master,
222 },
223
224 ParserState::Year => match ev {
225 Event::Text(e) => {
226 self.current_item.year = e.unescape()?.parse()?;
227 ParserState::Year
228 }
229 _ => ParserState::Master,
230 },
231
232 ParserState::Videos => match ev {
233 Event::End(e) if e.local_name().as_ref() == b"videos" => ParserState::Master,
234
235 ev => {
236 self.videos_parser.process(ev)?;
237 if self.videos_parser.item_ready {
238 self.current_item.videos.push(self.videos_parser.take());
239 }
240 ParserState::Videos
241 }
242 },
243 };
244
245 Ok(())
246 }
247}
248
249pub struct MasterBuilder {
250 inner: Master,
251}
252
253impl MasterBuilder {
254 pub fn id(mut self, id: u32) -> Self {
255 self.inner.id = id;
256 self
257 }
258
259 pub fn title(mut self, title: &str) -> Self {
260 self.inner.title = title.to_string();
261 self
262 }
263
264 pub fn main_release(mut self, id: u32) -> Self {
265 self.inner.main_release = id;
266 self
267 }
268
269 pub fn notes(mut self, notes: &str) -> Self {
270 self.inner.notes = Some(notes.to_string());
271 self
272 }
273
274 pub fn year(mut self, year: u16) -> Self {
275 self.inner.year = year;
276 self
277 }
278
279 pub fn genre(mut self, genre: &str) -> Self {
280 self.inner.genres.push(genre.to_string());
281 self
282 }
283
284 pub fn style(mut self, style: &str) -> Self {
285 self.inner.styles.push(style.to_string());
286 self
287 }
288
289 pub fn data_quality(mut self, data_quality: &str) -> Self {
290 self.inner.data_quality = data_quality.to_string();
291 self
292 }
293
294 pub fn artist(mut self, credit: ArtistCreditBuilder) -> Self {
295 self.inner.artists.push(credit.build());
296 self
297 }
298
299 pub fn image(mut self, ty: &str, width: i16, height: i16) -> Self {
300 self.inner.images.push(Image {
301 r#type: ty.to_string(),
302 uri: None,
303 uri150: None,
304 width,
305 height,
306 });
307 self
308 }
309
310 pub fn video(mut self, src: &str, duration: u32, title: &str, description: &str) -> Self {
311 self.inner.videos.push(Video {
312 src: src.to_string(),
313 duration,
314 title: title.to_string(),
315 description: description.to_string(),
316 embed: true,
317 });
318 self
319 }
320
321 pub fn build(self) -> Master {
322 self.inner
323 }
324}
325#[cfg(test)]
326mod tests {
327 use pretty_assertions::assert_eq;
328 use std::io::{BufRead, BufReader, Cursor};
329
330 use crate::artist_credit::{ArtistCredit, ArtistCreditBuilder};
331
332 use super::{Master, MastersReader};
333
334 fn parse(xml: &'static str) -> Master {
335 let reader: Box<dyn BufRead> = Box::new(BufReader::new(Cursor::new(xml)));
336 let mut reader = quick_xml::Reader::from_reader(reader);
337 reader.config_mut().trim_text(true);
338 MastersReader::new(reader, Vec::new()).next().unwrap()
339 }
340
341 fn credit(id: u32, name: &str) -> ArtistCreditBuilder {
342 ArtistCredit::builder(id, name)
343 }
344
345 #[test]
346 fn test_master_830352_20231001() {
347 let expected = Master::builder(830352, "What Is The Time, Mr. Templar? / The Real Jazz")
348 .main_release(1671981)
349 .year(2009)
350 .genre("Electronic")
351 .style("Techno")
352 .style("Tech House")
353 .data_quality("Correct")
354 .artist(credit(239, "Jesper Dahlbäck").join("/"))
355 .artist(credit(1654, "DK").anv("Dahlbäck & Krome"))
356 .image("primary", 600, 600)
357 .image("secondary", 600, 600)
358 .video(
359 "https://www.youtube.com/watch?v=1andhkV72eo",
360 311,
361 "JESPER DAHLBÄCK - WHAT IS THE TIME, MR. TEMPLAR? (PND02)",
362 "JESPER DAHLBÄCK - WHAT IS THE TIME, MR. TEMPLAR? (PND02)\n\n#pvnx #thismustbethetrack #trackoftheday #russianadvisor #bvckgrnd #vinyl #vinylcommunity #vinylcollection #recordcollector #vinyladdict #records #music #musiclover #musicblog #vinyloftheday #mus",
363 )
364 .video(
365 "https://www.youtube.com/watch?v=IWRv_Ye03cU",
366 315,
367 "J Dahlbäck - The Persuader - What Is The Time, Mr Templar?",
368 "From the LP: J. Dahlbäck - The Persuader - Label: Svek - Released: 1997\n\nProblem with the video? Please tell me and it will be removed immediately!"
369 )
370 .build();
371 let parsed = parse(
372 r#"
373<master id="830352">
374 <main_release>1671981</main_release>
375 <images>
376 <image type="primary" uri="" uri150="" width="600" height="600"/>
377 <image type="secondary" uri="" uri150="" width="600" height="600"/>
378 </images>
379 <artists>
380 <artist>
381 <id>239</id>
382 <name>Jesper Dahlbäck</name>
383 <anv>
384 </anv>
385 <join>/</join>
386 <role>
387 </role>
388 <tracks>
389 </tracks>
390 </artist>
391 <artist>
392 <id>1654</id>
393 <name>DK</name>
394 <anv>Dahlbäck & Krome</anv>
395 <join>
396 </join>
397 <role>
398 </role>
399 <tracks>
400 </tracks>
401 </artist>
402 </artists>
403 <genres>
404 <genre>Electronic</genre>
405 </genres>
406 <styles>
407 <style>Techno</style>
408 <style>Tech House</style>
409 </styles>
410 <year>2009</year>
411 <title>What Is The Time, Mr. Templar? / The Real Jazz</title>
412 <data_quality>Correct</data_quality>
413 <videos>
414 <video src="https://www.youtube.com/watch?v=1andhkV72eo" duration="311" embed="true">
415 <title>JESPER DAHLBÄCK - WHAT IS THE TIME, MR. TEMPLAR? (PND02)</title>
416 <description>JESPER DAHLBÄCK - WHAT IS THE TIME, MR. TEMPLAR? (PND02)
417
418#pvnx #thismustbethetrack #trackoftheday #russianadvisor #bvckgrnd #vinyl #vinylcommunity #vinylcollection #recordcollector #vinyladdict #records #music #musiclover #musicblog #vinyloftheday #mus</description>
419 </video>
420 <video src="https://www.youtube.com/watch?v=IWRv_Ye03cU" duration="315" embed="true">
421 <title>J Dahlbäck - The Persuader - What Is The Time, Mr Templar?</title>
422 <description>From the LP: J. Dahlbäck - The Persuader - Label: Svek - Released: 1997
423
424Problem with the video? Please tell me and it will be removed immediately!</description>
425 </video>
426 </videos>
427</master>
428 "#,
429 );
430 assert_eq!(expected, parsed);
431 }
432}