ddex_parser/transform/
flatten.rs

1// core/src/transform/flatten.rs
2//! Graph to flat model transformation
3
4use 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, // Set to 0 if no tracks
28            deal_count: graph.deals.len(),
29            total_duration: 0, // Set to 0 if no duration
30        };
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    // Helper methods
181    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}