"""Approval logic.""" from datetime import datetime from typing import Any from zoneinfo import ZoneInfo from flask import g from sqlalchemy.orm import Session from video.connectors import mysql from video.constants import ( approval as approval_constants, error as error_constants, release as release_constants, ) from video.exceptions import ( InvalidRequest, ProductVideoDataError, ProductVideoNotFound, VideoValidationError, ) from video.models.ows import ( artist as ows_artist, product as ows_product, track as ows_track, ) from video.models.sql.classes import approval, product_video, release from video.utils import status_checker def get(product_id: int) -> dict[str, Any]: """Get approvals.""" return approval.get(product_id) def change( product_id: int, user_id: str | None, data: dict[str, Any] ) -> dict[str, Any]: """Update an approval. data['approval_type'] can be [final, rejection, release, content]. """ approval_in_progress = data.get("approval_in_progress", False) approval_type = data["approval_type"] value = data["value"] if approval_type != approval_constants.REJECTION_TYPE and isinstance(value, str): normalized_value = value.lower() if normalized_value == "true": value = True elif normalized_value == "false": value = False else: raise InvalidRequest( "Invalid value for approval update: expected 'true' or 'false'." ) bypass_validation = False if data.get("bypass_validation"): bypass_validation = data["bypass_validation"] with mysql.ar_db_session() as session: # This locks the product_video row to prevent race conditions product_video.get( product_id, session=session, for_update=True, ) if bypass_validation: not_for_distribution = _get_product_not_for_distribution( product_id, session=session ) if not_for_distribution == "N": raise InvalidRequest( error_constants.ERROR_MESSAGE_CANNOT_BYPASS_VALIDATION ) else: if ( approval_type == approval_constants.FINAL_APPROVAL_TYPE and value is True ): valid_statuses = frozenset( { release_constants.TRANSFER_TO_CONTENT, release_constants.IN_CONTENT, } ) else: valid_statuses = frozenset({release_constants.TRANSFER_TO_CONTENT}) release_data = status_checker.check_release_status_before_action( product_id, valid_statuses, session=session, ) if release_data["release_status"] == release_constants.IN_CONTENT: return release_data if approval_type == approval_constants.FINAL_APPROVAL_TYPE: _update_upc_and_isrc_if_empty( product_id, data, session=session, ) return _final_approval( product_id, user_id, bypass_validation, approval_in_progress, session=session, ) approval_response = approval.upsert( product_id, user_id, approval_type, value, session=session, ) if approval_type == approval_constants.REJECTION_TYPE: return release.update( product_id, {"release_status": release_constants.LABEL_PROCESSING}, session=session, ) return approval_response def _map_video_data_to_track(video_data: dict[str, Any]) -> dict[str, Any]: """Map data from product_video table to input for ows_track.""" track_data = { "track_type": "video", "isrc": video_data["isrc"], "version": video_data["version"], "upc": video_data["upc"], "lyrics": video_data["lyrics"], "track_name": video_data["video_title"], "preview_start_time": video_data["preview_start_time"], } video_to_track_advisory = {"Yes": "Y", "No": "N", "Clean Version": "C"} try: parental_advisory = video_to_track_advisory[video_data["parental_advisory"]] track_data["explicit"] = parental_advisory except KeyError: pass if video_data["p_line_year"] and video_data["p_line_copyright_holder"]: track_data["p_info"] = "{} {}".format( video_data["p_line_year"], video_data["p_line_copyright_holder"] ) return track_data def update_track(product_id: int, video_data: dict[str, Any]) -> dict[str, Any]: """Update the track table.""" track_data = _map_video_data_to_track(video_data) track_id = ows_track.get_track_id(product_id) return ows_track.update_track(track_id, track_data) def update_genre(product_id: int, video_data: dict[str, Any]) -> None: """Update release_subgenre with subgenre info.""" subgenre_id = video_data["subgenre_id"] if subgenre_id is None: ows_product.delete_subgenre(product_id) return data = {"subgenre_id": subgenre_id, "upc": video_data["upc"]} subgenre = ows_product.get_subgenre(product_id) if not subgenre: ows_product.create_subgenre(product_id, data) else: ows_product.update_subgenre(product_id, data) def _map_video_data_to_product_contributors( video_data: dict[str, Any], ) -> list[dict[str, Any]]: """Map data from product_video table to input for ows_product.""" contributors = video_data["contributors"] or [] primary_artist = ows_artist.get_artist(video_data["primary_artist_id"]) primary_artist_contributor = { "name": primary_artist["name"], "role": "performer", "artist_info_id": primary_artist["id"], } return [ { "artist_name": contributor["name"], "role": contributor["role"], "artist_info_id": contributor.get("artist_info_id"), "upc": video_data["upc"], } for contributor in contributors + [primary_artist_contributor] ] def update_contributors(product_id: int, video_data: dict[str, Any]) -> None: """Update release_artist with contributors info.""" contributors = _map_video_data_to_product_contributors(video_data) ows_product.delete_contributors(product_id) for contributor in contributors: ows_product.create_contributor(product_id, contributor) def write_to_delivery_tables( product_id: int, video_data: dict[str, Any] | None = None, *, session: Session ) -> None: """Write data from product_video to tables used for delivery.""" if not video_data: video_data = product_video.get(product_id, session=session) if not video_data: raise ProductVideoNotFound() update_track(product_id, video_data) update_genre(product_id, video_data) update_contributors(product_id, video_data) def validate(product_id: int, video_data: dict[str, Any]) -> list[str]: """Validate product before approval.""" track_response = ows_track.is_isrc_used(video_data["isrc"], "video") errors: list[str] = [] if video_data["channel_selection"] == approval_constants.BAD_CHANNEL: errors.append("badChannel") # TODO: Check logic (DISTRO-4851) used_isrc_not_for_distribution: str | None = None if track_response.get("used"): used_by_product_id: int | None = track_response.get("used_by_product_id") used_product_data = ( release.get(used_by_product_id) if used_by_product_id else {} ) used_isrc_not_for_distribution = used_product_data.get("not_for_distribution") if ( track_response.get("used") and track_response.get("used_by_product_id") != product_id and video_data["not_for_distribution"] != "AccountingDummy" and used_isrc_not_for_distribution != "AccountingDummy" ): errors.append("videoIsrcInUse") return errors def _update_upc_and_isrc_if_empty( product_id: int, video_data: dict[str, Any], *, session: Session ) -> None: """ Create UPC and ISRC if empty on approval. Note that this code conflicts with the creation/update of the track that occurs in ows-track due to them both writing to the cloudsearch release corpus. Therefore, we generate new UPCs and ISRCs if necessary outside of the session context and write to the releases table too. The write to product_video still occurs in-session because that row is locked in the session. This means that in an error situation where the session is rolled back, the release will have an UPC set that is not on the product_video row. This is OK because the release will not be in_content, and to get it to that state a new UPC will be generated here and will overwrite the old UPC. """ update_fields = {} if not video_data.get("upc"): update_fields["display_upc"] = update_fields["upc"] = release.get_upc( session=None, ) if not video_data.get("isrc"): update_fields["isrc"] = release.get_isrc(session=None) if len(update_fields.keys()) > 0: product_video.upsert( { "release_id": product_id, **update_fields, }, session=session, ) release.update( product_id, update_fields, session=None, ) def _final_approval( product_id: int, user_id: str | None, bypass_validation: bool, approval_in_progress: bool, *, session: Session, ) -> dict[str, Any]: video_data = product_video.get(product_id, session=session) if not video_data: raise ProductVideoNotFound() if not bypass_validation: validation_errors = validate(product_id, video_data) if validation_errors: raise VideoValidationError(validation_errors) if not approval_in_progress: approval.upsert( product_id, user_id, approval_constants.FINAL_APPROVAL_TYPE, True, session=session, ) tz = ZoneInfo("America/New_York") ingestion_completed_time = datetime.now(tz).replace(microsecond=0) return release.update( product_id, { "release_status": release_constants.IN_CONTENT, "ingestion_completed": ingestion_completed_time, }, session=session, ) write_to_delivery_tables( product_id, video_data, session=session, ) return release.get(product_id, session=session) def _get_product_not_for_distribution( product_id: int, *, session: Session | None = None ) -> str: video_data = product_video.get(product_id, session=session) if not video_data: raise ProductVideoNotFound() value = video_data.get("not_for_distribution") if value is None: raise ProductVideoDataError( f"not_for_distribution is null for product {product_id}" ) return str(value) def revert_label_mgr_approval(product_id: int) -> None: """Revert the label manager approval. This function is used to revert label manager approval because an ISRC conflict. """ # Call logic.approval.change for release column only. change( product_id, None, data={"approval_type": approval_constants.RELEASE_TYPE, "value": None}, ) g.log.info( f"Reverted release approval (label manager) for product id: {product_id} due to ISRC conflict" )