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