"""Logic for Processor. Extracts product numbers and GRids from a database release and a GRAS release and compares them to determine what needs to be done for proper identifiers to exist in the database. If database already has identifiers, there is no need for action. If not, we either take identifiers from GRAS, if they exist, or generate new ones and save to database. """ from typing import Any from gridgen.models.datasource import orchard from gridgen.models.metadata import ( GridMeta, Release, ReleaseIdMeta, ReleaseMeta, TrackIdMeta, TrackMeta, ) EMPTY_GRID_META = GridMeta(None, None) TRACK_DURATION_THRESHOLD = 10 def synch_release(ar_release: Release, gras_release: Release) -> bool: """Populate database with proper identifiers for release. Only if GRAS and database identifiers do not match do we need to send a delivery message. Args: ar_release (Release): the release data from Orchard database. gras_release (Release): the release data from Gras webservice. Returns: bool: indicates whether a delivery message should be sent """ if not ar_release.metadata: return False release_id_meta = ReleaseIdMeta( ar_release.metadata.release_id, ar_release.metadata.upc ) ar_grid_meta = extract_release_grid(ar_release.metadata) gras_grid_meta = extract_release_grid(gras_release.metadata) # if no grid info from both Art Relations and GRAS and there is only if ar_grid_meta == EMPTY_GRID_META and gras_grid_meta == EMPTY_GRID_META: if ar_release.metadata.format == "physical": # physical release, use upc for product # for ar_grid_meta ar_grid_meta = GridMeta(None, ar_release.metadata.upc[:-1]) elif len(ar_release.tracks) == 1: # 1 track, lookup track and use track grid info release_track_isrcs = list(ar_release.tracks.keys()) updated_track = orchard.lookup_track( TrackIdMeta( ar_release.tracks[release_track_isrcs[0]].track_id, release_track_isrcs[0], ar_release.metadata.upc, ) ) gras_grid_meta = extract_track_grid(updated_track) return save_grid(ar_grid_meta, gras_grid_meta, release_id_meta) def synch_track( ar_release: Release, gras_release: Release, track_id_meta: TrackIdMeta ) -> bool: """Populate database with proper identifiers for track. Only if GRAS and database identifiers do not match do we need to send a delivery message. Args: ar_release (Release): the release data from Orchard database. gras_release (Release): the release data from Gras webservice. track_id_meta (TrackIdMeta): identifier info of track Returns: bool: indicates whether a delivery message should be sent """ ar_track = ar_release.get_track_by_isrc(track_id_meta.isrc) gras_track = gras_release.get_track_by_isrc(track_id_meta.isrc) if not ar_track: return False ar_grid_meta = extract_track_grid(ar_track) gras_grid_meta = extract_track_grid(gras_track) return save_grid(ar_grid_meta, gras_grid_meta, track_id_meta) def extract_release_grid(release_meta: ReleaseMeta | None) -> GridMeta: """Construct a GridMeta tuple from release metadata. Args: release_meta (ReleaseMeta): the release data. Returns: GridMeta: named tuple containing grid and product number. """ if release_meta: return GridMeta(release_meta.grid, release_meta.product_number) return EMPTY_GRID_META def extract_track_grid(track_meta: TrackMeta | None) -> GridMeta: """Construct a GridMeta tuple from track metadata. Args: track_meta (TrackMeta | None): the track data. Returns: GridMeta: named tuple containing grid and product number. """ if track_meta and track_meta.product_numbers: return next(iter(track_meta.product_numbers)) return EMPTY_GRID_META def save_grid( ar_grid_meta: GridMeta, gras_grid_meta: GridMeta, id_meta: TrackIdMeta | ReleaseIdMeta, ) -> bool: """Populate database with proper grid and product numbers. Args: ar_grid_meta (GridMeta): the grid data from orchard database. gras_grid_meta (GridMeta): the grid data from GRAS webservice. id_meta (TrackIdMeta | ReleaseIdMeta): identifier info of track or release Returns: bool: indicates whether new GRids were generated or database GRids do not match GRAS webservice GRids """ gras_update_needed = True if gras_grid_meta.product_no: gras_update_needed = False save_grid_meta = gras_grid_meta elif ar_grid_meta.product_no: save_grid_meta = ar_grid_meta else: return gras_update_needed if type(id_meta) is TrackIdMeta: orchard.save_track(id_meta, save_grid_meta) elif type(id_meta) is ReleaseIdMeta: orchard.save_release(id_meta, save_grid_meta) return gras_update_needed def validate_release(ar_release: Release, gras_release: Release) -> dict[str, Any]: """Compare release metadata and returns result of comparison. Args: ar_release (Release): the release data from Orchard database. gras_release (Release): the release data from Gras webservice. Returns: dict: contains track identifiers and errors indicated by metadata comparison """ if not ar_release.metadata: return {} if not gras_release.metadata: return {} grid_meta = extract_release_grid(gras_release.metadata) error_metadata = compare_release_fields(ar_release.metadata, gras_release.metadata) result = { "upc": ar_release.metadata.upc, "grid": grid_meta.grid, "product_no": grid_meta.product_no, "errors": {"metadata": error_metadata, "general": []}, } return {"id": ar_release.metadata.release_id, "data": result} def validate_track( ar_release: Release, gras_release: Release, id_meta: TrackIdMeta, ) -> dict[str, Any]: """Compare track metadata and returns result of comparison. Args: ar_release (Release): the release data from Orchard database. gras_release (Release): the release data from Gras webservice. id_meta (TrackIdMeta): identifier info of track Returns: dict: contains track identifiers and errors indicated by metadata comparison """ ar_track = ar_release.get_track_by_isrc(id_meta.isrc) gras_track = gras_release.get_track_by_isrc(id_meta.isrc) if not ar_track: return {} if not gras_track: return {} grid_meta = extract_track_grid(gras_track) error_metadata = compare_track_fields(ar_track, gras_track) result = { "isrc": id_meta.isrc, "grid": grid_meta.grid, "product_no": grid_meta.product_no, "errors": {"metadata": error_metadata, "general": []}, } return {"id": id_meta.track_id, "data": result} def compare_release_fields( ar_release: ReleaseMeta, gras_release: ReleaseMeta ) -> list[dict[str, Any]]: """Check existence of release metadata fields in GRAS release. Args: ar_release (ReleaseMeta): the release data from Orchard database. gras_release (ReleaseMeta): the release data from Gras webservice. Returns: list: all comparison errors that will be reported """ errors = [] # comparison for release_name if gras_release.release_name: if ( not ar_release.release_name or gras_release.release_name.lower() != ar_release.release_name.lower() ): errors.append( { "error_code": "M01", "field": "release_name", "message": gras_release.release_name, } ) # comparison for display_artist_name if gras_release.display_artist_name: if ( not ar_release.display_artist_name or gras_release.display_artist_name.lower() != ar_release.display_artist_name.lower() ): errors.append( { "error_code": "M01", "field": "display_artist_name", "message": gras_release.display_artist_name, } ) return errors def compare_track_fields( ar_track: TrackMeta, gras_track: TrackMeta ) -> list[dict[str, Any]]: """Check existence of track metadata fields in GRAS track. Args: ar_track (TrackMeta): the track data from Orchard database. gras_track (TrackMeta): the track data from Gras webservice. Returns: list: all comparison errors that will be reported """ errors: list[dict[str, Any]] = [] # comparison for main_participants if gras_track.main_participants: participants = [] ar_participants = [] for artist in gras_track.main_participants: participants.append("{} ({})".format(artist.name, artist.role)) for ar_artists in ar_track.main_participants: ar_participants.append("{} ({})".format(ar_artists.name, ar_artists.role)) if ",".join(ar_participants).lower() != ",".join(participants).lower(): errors.append( { "error_code": "M01", "field": "main_participants", "message": ",".join(participants), } ) # comparison for track_name if gras_track.track_name: if gras_track.track_name.lower() != ar_track.track_name.lower(): errors.append( { "error_code": "M01", "field": "track_name", "message": gras_track.track_name, } ) # comparison for Track version if gras_track.track_supplemental_info: if ( gras_track.track_supplemental_info.lower() != ar_track.track_supplemental_info.lower() ): errors.append( { "error_code": "M01", "field": "track_supplemental_info", "message": gras_track.track_supplemental_info, } ) # comparison for p-line. Ignore starting p if it exists if gras_track.p_line: gras_p_line = gras_track.p_line.lower() if gras_p_line.startswith("(p) "): gras_p_line = gras_p_line[4:] if not ar_track.p_line: errors.append( { "error_code": "M01", "field": "p_line", "message": "Orchard track P-line not found.", } ) else: ar_p_line = ar_track.p_line.lower() if ar_p_line.startswith("(p) "): ar_p_line = ar_p_line[4:] if gras_p_line != ar_p_line: errors.append( { "error_code": "M01", "field": "p_line", "message": gras_track.p_line, } ) # comparison for duration (playtime). difference should not exceed 10 secs if gras_track.duration: duration_difference = abs(int(gras_track.duration) - int(ar_track.duration)) if duration_difference >= TRACK_DURATION_THRESHOLD: errors.append( { "error_code": "M01", "field": "duration", "message": gras_track.duration, } ) # comparison for track_type (track_media) if gras_track.track_type: if gras_track.track_type.lower() != ar_track.track_type.lower(): errors.append( { "error_code": "M01", "field": "track_type", "message": gras_track.track_type, } ) return errors