1use crate::error::ParseError;
5
6type Result<T> = std::result::Result<T, ParseError>;
7use ddex_core::models::common::{Identifier, LocalizedString};
8use ddex_core::models::flat::{
9 ArtistInfo, DealValidity, DistributionComplexity, FlattenedMessage, MessageStats, Organization,
10 ParsedDeal, ParsedRelease, ParsedResource, ParsedTrack, PriceTier, PriceType, ProprietaryId,
11 ReleaseIdentifiers, TechnicalInfo, TerritoryComplexity, TerritoryInfo,
12};
13use ddex_core::models::graph::{
14 Artist, Deal, DealTerms, ERNMessage, Party, Release, ReleaseResourceReference, Resource,
15};
16use indexmap::IndexMap;
17use std::collections::HashMap;
18
19pub struct Flattener;
20
21impl Flattener {
22 pub fn flatten(graph: ERNMessage) -> Result<FlattenedMessage> {
23 let releases = Self::flatten_releases(&graph.releases, &graph.resources)?;
24 let resources = Self::flatten_resources(&graph.resources)?;
25 let deals = Self::flatten_deals(&graph.deals)?;
26 let parties = Self::flatten_parties(&graph.parties)?;
27
28 let stats = MessageStats {
29 release_count: graph.releases.len(),
30 track_count: 0, deal_count: graph.deals.len(),
32 total_duration: 0, };
34
35 Ok(FlattenedMessage {
36 message_id: graph.message_header.message_id.clone(),
37 message_type: format!("{:?}", graph.message_header.message_type),
38 message_date: graph.message_header.message_created_date_time,
39 sender: Organization {
40 name: Self::get_primary_name(&graph.message_header.message_sender.party_name, "MessageSender/PartyName")?,
41 id: Self::get_primary_id(&graph.message_header.message_sender.party_id, "MessageSender/PartyId")?,
42 extensions: None,
43 },
44 recipient: Organization {
45 name: Self::get_primary_name(&graph.message_header.message_recipient.party_name, "MessageRecipient/PartyName")?,
46 id: Self::get_primary_id(&graph.message_header.message_recipient.party_id, "MessageRecipient/PartyId")?,
47 extensions: None,
48 },
49 releases,
50 resources,
51 deals,
52 parties,
53 version: format!("{:?}", graph.version),
54 profile: graph.profile.map(|p| format!("{:?}", p)),
55 stats,
56 extensions: None,
57 })
58 }
59
60 fn flatten_releases(releases: &[Release], resources: &[Resource]) -> Result<Vec<ParsedRelease>> {
61 releases
62 .iter()
63 .map(|release| Ok(ParsedRelease {
64 release_id: release.release_reference.clone(),
65 identifiers: Self::extract_identifiers(&release.release_id),
66 title: release.release_title.clone(),
67 default_title: Self::get_primary_title(&release.release_title, "Release/Title/TitleText")?,
68 subtitle: release.release_subtitle.clone(),
69 default_subtitle: release
70 .release_subtitle
71 .as_ref()
72 .map(|s| Self::get_primary_title_optional(s))
73 .flatten(),
74 display_artist: Self::format_display_artist(&release.display_artist)?,
75 artists: Self::extract_artists(&release.display_artist)?,
76 release_type: release
77 .release_type
78 .as_ref()
79 .map(|t| format!("{:?}", t))
80 .ok_or_else(|| ParseError::MissingField("Release/ReleaseType".to_string()))?,
81 genre: release.genre.first().map(|g| g.genre_text.clone()),
82 sub_genre: release.genre.first().and_then(|g| g.sub_genre.clone()),
83 tracks: Self::build_tracks(&release.release_resource_reference_list, resources)?,
84 track_count: release.release_resource_reference_list.len(),
85 disc_count: Self::count_discs(&release.release_resource_reference_list),
86 videos: Vec::new(),
87 images: Vec::new(),
88 cover_art: None,
89 release_date: release.release_date.first().and_then(|e| e.event_date),
90 original_release_date: None,
91 territories: Self::build_territories(
92 &release.territory_code,
93 &release.excluded_territory_code,
94 ),
95 p_line: None,
96 c_line: None,
97 parent_release: None,
98 child_releases: Vec::new(),
99 extensions: None,
100 }))
101 .collect()
102 }
103
104 fn flatten_resources(resources: &[Resource]) -> Result<IndexMap<String, ParsedResource>> {
105 resources
106 .iter()
107 .map(|resource| {
108 let parsed = ParsedResource {
109 resource_id: resource.resource_reference.clone(),
110 resource_type: format!("{:?}", resource.resource_type),
111 title: Self::get_primary_title(&resource.reference_title, "Resource/ReferenceTitle/TitleText")?,
112 duration: resource.duration,
113 technical_details: TechnicalInfo {
114 file_format: resource
115 .technical_details
116 .first()
117 .and_then(|t| t.file_format.clone()),
118 bitrate: resource.technical_details.first().and_then(|t| t.bitrate),
119 sample_rate: resource
120 .technical_details
121 .first()
122 .and_then(|t| t.sample_rate),
123 file_size: resource.technical_details.first().and_then(|t| t.file_size),
124 },
125 };
126 Ok((resource.resource_reference.clone(), parsed))
127 })
128 .collect()
129 }
130
131 fn flatten_deals(deals: &[Deal]) -> Result<Vec<ParsedDeal>> {
132 deals
133 .iter()
134 .map(|deal| Ok(ParsedDeal {
135 deal_id: deal
136 .deal_reference
137 .clone()
138 .ok_or_else(|| ParseError::MissingField("Deal/DealReference".to_string()))?,
139 releases: deal.deal_release_reference.clone(),
140 validity: DealValidity {
141 start: deal.deal_terms.start_date,
142 end: deal.deal_terms.end_date,
143 },
144 territories: TerritoryComplexity {
145 included: deal.deal_terms.territory_code.clone(),
146 excluded: deal.deal_terms.excluded_territory_code.clone(),
147 },
148 distribution_channels: DistributionComplexity {
149 included: deal
150 .deal_terms
151 .distribution_channel
152 .iter()
153 .map(|c| format!("{:?}", c))
154 .collect(),
155 excluded: deal
156 .deal_terms
157 .excluded_distribution_channel
158 .iter()
159 .map(|c| format!("{:?}", c))
160 .collect(),
161 },
162 pricing: Self::build_price_tiers(&deal.deal_terms),
163 usage_rights: deal
164 .deal_terms
165 .use_type
166 .iter()
167 .map(|u| format!("{:?}", u))
168 .collect(),
169 restrictions: Vec::new(),
170 }))
171 .collect()
172 }
173
174 fn flatten_parties(parties: &[Party]) -> Result<IndexMap<String, Party>> {
175 parties
176 .iter()
177 .map(|party| {
178 let id = Self::get_primary_id(&party.party_id, "Party/PartyId")?;
179 Ok((id, party.clone()))
180 })
181 .collect()
182 }
183
184 fn get_primary_name(names: &[LocalizedString], field_path: &str) -> Result<String> {
186 names
187 .first()
188 .map(|n| n.text.clone())
189 .ok_or_else(|| ParseError::MissingField(field_path.to_string()))
190 }
191
192 fn get_primary_title(titles: &[LocalizedString], field_path: &str) -> Result<String> {
193 titles
194 .first()
195 .map(|t| t.text.clone())
196 .ok_or_else(|| ParseError::MissingField(field_path.to_string()))
197 }
198
199 fn get_primary_id(ids: &[Identifier], field_path: &str) -> Result<String> {
200 ids.first()
201 .map(|id| id.value.clone())
202 .ok_or_else(|| ParseError::MissingField(field_path.to_string()))
203 }
204
205 fn get_primary_name_optional(names: &[LocalizedString]) -> Option<String> {
207 names.first().map(|n| n.text.clone())
208 }
209
210 fn get_primary_title_optional(titles: &[LocalizedString]) -> Option<String> {
211 titles.first().map(|t| t.text.clone())
212 }
213
214 fn get_primary_id_optional(ids: &[Identifier]) -> Option<String> {
215 ids.first().map(|id| id.value.clone())
216 }
217
218 #[allow(dead_code)]
219 fn count_tracks(releases: &[ParsedRelease]) -> usize {
220 releases.iter().map(|r| r.track_count).sum()
221 }
222
223 #[allow(dead_code)]
224 fn calculate_total_duration(resources: &HashMap<String, ParsedResource>) -> u64 {
225 resources
226 .values()
227 .filter_map(|r| r.duration)
228 .map(|d| d.as_secs())
229 .sum()
230 }
231
232 fn extract_identifiers(ids: &[Identifier]) -> ReleaseIdentifiers {
233 let mut identifiers = ReleaseIdentifiers {
234 upc: None,
235 ean: None,
236 catalog_number: None,
237 grid: None,
238 proprietary: Vec::new(),
239 };
240
241 for id in ids {
242 match &id.id_type {
243 ddex_core::models::common::IdentifierType::UPC => {
244 identifiers.upc = Some(id.value.clone())
245 }
246 ddex_core::models::common::IdentifierType::EAN => {
247 identifiers.ean = Some(id.value.clone())
248 }
249 ddex_core::models::common::IdentifierType::GRID => {
250 identifiers.grid = Some(id.value.clone())
251 }
252 ddex_core::models::common::IdentifierType::Proprietary => {
253 if let Some(ns) = &id.namespace {
254 identifiers.proprietary.push(ProprietaryId {
255 namespace: ns.clone(),
256 value: id.value.clone(),
257 });
258 }
259 }
260 _ => {}
261 }
262 }
263
264 identifiers
265 }
266
267 fn format_display_artist(artists: &[Artist]) -> Result<String> {
268 let names: Result<Vec<String>> = artists
269 .iter()
270 .map(|a| Self::get_primary_name(&a.display_artist_name, "Artist/DisplayArtistName"))
271 .collect();
272 Ok(names?.join(", "))
273 }
274
275 fn extract_artists(artists: &[Artist]) -> Result<Vec<ArtistInfo>> {
276 artists
277 .iter()
278 .map(|artist| Ok(ArtistInfo {
279 name: Self::get_primary_name(&artist.display_artist_name, "Artist/DisplayArtistName")?,
280 role: artist
281 .artist_role
282 .first()
283 .cloned()
284 .ok_or_else(|| ParseError::MissingField("Artist/ArtistRole".to_string()))?,
285 party_id: artist.party_reference.clone(),
286 }))
287 .collect()
288 }
289
290 fn build_tracks(refs: &[ReleaseResourceReference], resources: &[Resource]) -> Result<Vec<ParsedTrack>> {
291 refs.iter()
292 .enumerate()
293 .map(|(idx, rref)| {
294 let resource = resources
295 .iter()
296 .find(|r| r.resource_reference == rref.resource_reference);
297
298 let title = match resource {
300 Some(r) => Self::get_primary_title_optional(&r.reference_title),
301 None => None,
302 };
303
304 Ok(ParsedTrack {
305 track_id: rref.resource_reference.clone(),
306 isrc: resource.and_then(|r| {
307 r.resource_id
308 .iter()
309 .find(|id| {
310 matches!(
311 id.id_type,
312 ddex_core::models::common::IdentifierType::ISRC
313 )
314 })
315 .map(|id| id.value.clone())
316 }),
317 iswc: None,
318 position: idx + 1,
319 track_number: rref.track_number,
320 disc_number: rref.disc_number,
321 side: rref.side.clone(),
322 title: title.ok_or_else(|| ParseError::MissingField("Resource/ReferenceTitle/TitleText".to_string()))?,
323 subtitle: None,
324 display_artist: String::new(),
325 artists: Vec::new(),
326 duration: resource
327 .and_then(|r| r.duration)
328 .ok_or_else(|| ParseError::MissingField("Resource/Duration".to_string()))?,
329 duration_formatted: resource
330 .and_then(|r| r.duration)
331 .map(ParsedTrack::format_duration)
332 .ok_or_else(|| ParseError::MissingField("Resource/Duration".to_string()))?,
333 file_format: None,
334 bitrate: None,
335 sample_rate: None,
336 is_hidden: rref.is_hidden,
337 is_bonus: rref.is_bonus,
338 is_explicit: false,
339 is_instrumental: false,
340 })
341 })
342 .collect()
343 }
344
345 fn count_discs(refs: &[ReleaseResourceReference]) -> Option<usize> {
346 refs.iter()
347 .filter_map(|r| r.disc_number)
348 .max()
349 .map(|n| n as usize)
350 }
351
352 fn build_territories(included: &[String], excluded: &[String]) -> Vec<TerritoryInfo> {
353 let mut territories = Vec::new();
354
355 for code in included {
356 territories.push(TerritoryInfo {
357 code: code.clone(),
358 included: true,
359 start_date: None,
360 end_date: None,
361 distribution_channels: Vec::new(),
362 });
363 }
364
365 for code in excluded {
366 territories.push(TerritoryInfo {
367 code: code.clone(),
368 included: false,
369 start_date: None,
370 end_date: None,
371 distribution_channels: Vec::new(),
372 });
373 }
374
375 territories
376 }
377
378 fn build_price_tiers(terms: &DealTerms) -> Vec<PriceTier> {
379 let mut tiers = Vec::new();
380
381 for price in &terms.wholesale_price {
382 tiers.push(PriceTier {
383 tier_name: None,
384 price_type: PriceType::Wholesale,
385 price: price.clone(),
386 territory: price.territory.clone(),
387 start_date: terms.start_date,
388 end_date: terms.end_date,
389 });
390 }
391
392 for price in &terms.suggested_retail_price {
393 tiers.push(PriceTier {
394 tier_name: None,
395 price_type: PriceType::SuggestedRetail,
396 price: price.clone(),
397 territory: price.territory.clone(),
398 start_date: terms.start_date,
399 end_date: terms.end_date,
400 });
401 }
402
403 tiers
404 }
405}