""" Application main handlers. Contains endpoint handlers responsible for asset processing based on AWS. """ import ast from typing import assert_never from flask import Response, g, jsonify, request from jsonschema import ValidationError, validate from owsrequest import context, flask_request from owsrequest.constants import errors, headers from owsresponse import response as ows_response from owsresponse.adaptors.flask import flaskify from requests.exceptions import HTTPError from werkzeug.exceptions import HTTPException from assets import config, rate_limiting from assets.api import app from assets.constants import ( asset_status as asset_status_constants, asset_types, asset_upload as asset_upload_constants, authorization, error, field_const, s3, ) from assets.exceptions import ( AssetDeleteFailure, AssetEncodingInProgress, AssetUploadError, ErrorDiscardingCorrections, JwtInvalid, JwtMissing, ) from assets.logic import ( ai_detection as ai_detection_logic, asset_copy, asset_info, asset_status, asset_upload, delete as delete_logic_v2, generate_upload_data, hive_ai_image_task, hive_segment, hive_text_recognition as hive_text_recognition_logic, image_location, match_audio as match_audio_logic, ownership, pdp_auth, product as product_logic_v2, stereo_reference, stream_info, track, validate_bit_depth_by_upc, validators, ) from assets.logic.legacy import ( image_location as image_location_legacy, ) from assets.rate_limiting import RateLimitedResource from assets.utils import ( handlers as handlers_utils, json_encoder, user as user_util, ) from assets.validation import input_value_validator, json_schema from assets.validation.requester import block_alw_and_oa_users from assets.validation.schema import body, query @app.route(config.HEALTH_CHECK, methods=["GET"]) def health() -> Response: """Check the health of the application.""" return jsonify({"status": "ok"}) @app.errorhandler(Exception) def exception_handler(exc: Exception) -> Response: """Default handler when an uncaught exception is raised. Note: Exception will also be sent to Sentry if config.SENTRY is set. Returns: Response: A 500 response with an error message. """ g.log.exception(exc) return flaskify( ows_response.create_fatal_response( message="The server encountered an internal error " "and was unable to complete your request." ) ) @app.errorhandler(HTTPException) def http_exception_handler(exc: HTTPException) -> Response: """Handle error when HTTPException is raised. Returns: Response: response with corresponding status code and error message from the exception. """ if exc.code == ows_response.status.NOT_FOUND: code = ows_response.error.ERROR_CODE_NOT_FOUND else: code = exc.name.replace(" ", "_").lower() if exc.code >= ows_response.status.INTERNAL_ERROR: app.logger.exception(exc) else: app.logger.info(exc) return flaskify( ows_response.create_error_response( code=code, message=exc.description, status=(exc.code or ows_response.status.INTERNAL_ERROR), ) ) @app.errorhandler(HTTPError) def http_error_handler(exc: HTTPError) -> Response: """Handle error when HTTPError is raised from upstream services. Returns: Response: A HTTP status BAD_GATEWAY code response with an error message. """ app.logger.exception(exc) return flaskify( ows_response.create_error_response( code="bad_gateway", message=str(exc), status=ows_response.status.BAD_GATEWAY ) ) @app.errorhandler(AssetUploadError) @app.errorhandler(AssetDeleteFailure) @app.errorhandler(ErrorDiscardingCorrections) def custom_exception_handler( exc: AssetDeleteFailure | AssetUploadError | ErrorDiscardingCorrections, ) -> Response: """Handle custom exceptions.""" g.log.warning(exc) try: message = ast.literal_eval(str(exc)) except Exception: message = str(exc) return flaskify(ows_response.create_fatal_response(message=message)) @app.errorhandler(AssetEncodingInProgress) def asset_encoding_in_progress_handler(exception: HTTPException) -> Response: """Default handler when the AssetEncodingInProgress exception is raised. Returns: Response: A 204 response. """ return flaskify( ows_response.Response( exception.description, status=ows_response.status.NO_CONTENT ) ) @app.errorhandler(JwtMissing) def jwt_missing_handler(_exception: JwtMissing) -> Response: return Response(status=ows_response.status.UNAUTHORIZED) @app.errorhandler(JwtInvalid) def jwt_invalid_handler(_exception: JwtInvalid) -> Response: return Response(status=ows_response.status.FORBIDDEN) @app.route("/v2/assets/upload", methods=["POST"]) @json_schema.validate_headers(request, required=True) @json_schema.validate_body(request, body.create_asset_upload_schema) def create_asset_upload() -> Response: """Create an asset upload. An asset_upload record will be created with asset, product, and track info, and an asset_status record will be created with status "uploading". An AWS S3 multipart upload will be initiated and the UploadId will be saved. Returns: Response: Unique filename for the asset upload. """ request_body = request.get_json() product_id = request_body["product_id"] user_id = request.headers[ field_const.ORCHARD_USER_ID ] # validate_headers will raise if missing resource = RateLimitedResource.UPLOAD key = user_id rate_limit_result = rate_limiting.get_rate_limiter().hit(resource, key) match rate_limit_result: case rate_limiting.Error(): g.log.error( f"Rate limiter error for {resource.value}:{key} - {rate_limit_result.message} - failing open" ) case rate_limiting.Exceeded(): return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_RATE_LIMIT_EXCEEDED, message=rate_limit_result.message, status=429, ) ) case rate_limiting.Allowed(): pass case _: assert_never(rate_limit_result) # PP shadow auth (CDAM-4044): measures the PP decision; always allows. pdp_auth.shadow_authorization( pdp_auth.ACTION_UPDATE, pdp_auth.tenant_from_product, product_id ) # Verify requester owns product ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) if request_body.get(field_const.ASSET_UPLOAD_TYPE) is None: # TODO: CDAM-3825: Remove once all callers send asset_upload_type g.log.info( f"Missing field '{field_const.ASSET_UPLOAD_TYPE}' in create_asset_upload request body (user_id={user_id})" ) filename = generate_upload_data.create_asset_upload( **request_body, user_id=user_id, ) return flaskify(ows_response.Response({"filename": filename})) @app.route("/v2/assets/upload/", methods=["GET"]) @json_schema.validate_headers(request, required=True) @json_schema.validate_query(request, query.get_presigned_urls_for_asset_upload_schema) def get_presigned_urls_for_asset_upload(filename: str) -> Response: """Get presigned urls for asset upload. Args: filename (str): filename of the asset upload. Returns: Response: Response containing presigned urls or error. """ # PP shadow auth (CDAM-4044): measures the PP decision; always allows. pdp_auth.shadow_authorization( pdp_auth.ACTION_UPDATE, pdp_auth.tenant_from_filename, filename ) part_numbers = [ int(part_number) for part_number in request.args[field_const.PART_NUMBERS].split(",") ] try: validate( instance=part_numbers, schema={ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", "uniqueItems": True, "minItems": 1, "maxItems": 20, "items": { "type": "integer", "minimum": 1, "maximum": 10000, }, }, ) except ValidationError as ve: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_QUERY_VALIDATION, message=str(ve), ), ) return flaskify( ows_response.Response( generate_upload_data.get_presigned_urls_for_asset_upload( part_numbers=part_numbers, filename=filename, user_id=request.headers[ field_const.ORCHARD_USER_ID ], # validate_headers will raise if missing ) ) ) @app.route("/v2/assets/upload/", methods=["PATCH"]) @json_schema.validate_headers(request, required=True) @json_schema.validate_body(request, body.complete_multipart_upload_schema) def complete_multipart_upload(filename: str) -> Response: """Complete a multipart upload. Args: filename (str): filename of the asset upload. Returns: Response """ # PP shadow auth (CDAM-4044): measures the PP decision; always allows. pdp_auth.shadow_authorization( pdp_auth.ACTION_UPDATE, pdp_auth.tenant_from_filename, filename ) request_body = request.get_json() generate_upload_data.complete_multipart_upload( filename=filename, user_id=request.headers[ field_const.ORCHARD_USER_ID ], # validate_headers will raise if missing parts=request_body[field_const.PARTS], ) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route("/upload-token", methods=["GET", "POST"]) @json_schema.validate_headers(request, required=False) @json_schema.validate_body(request, body.get_upload_token_schema) def get_upload_token() -> Response: """Generate data required for raw assets upload. Returns: Response: Response containing upload data or error. """ user_id = request.headers.get(field_const.ORCHARD_USER_ID) identity_id = request.headers.get(headers.ORCHARD_IDENTITY_ID) if not user_id and not identity_id: return flaskify(ows_response.Response("User is required", status=401)) if not user_id and not handlers_utils.is_jwt_identity_authorized(identity_id): return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message="Failed to identify user", status=401, ) ) if request.method == "GET": duration = int( request.args.get(field_const.DURATION, config.STS_TOKEN_DURATION) ) asset_type = request.args.get(field_const.ASSET_TYPE) else: data = request.get_json(silent=True, force=True) or {} duration = int(data.get(field_const.DURATION, config.STS_TOKEN_DURATION)) asset_type = data.get(field_const.ASSET_TYPE) if not asset_type: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message="Asset Type is required", status=400, ) ) asset_type = asset_type.strip().lower() if asset_type not in asset_upload_constants.ALLOWED_ASSET_TYPES: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message="Unsupported Asset Type", status=400, ) ) if duration < config.MIN_STS_TOKEN_DURATION: duration = config.MIN_STS_TOKEN_DURATION # mypy cannot use flow control to prove the value of user_id or identity_id is not # None. ignoring the type error for now. return flaskify( ows_response.Response( generate_upload_data.get_upload_permission( user_id or identity_id, # type: ignore[arg-type] duration, asset_type, ) ) ) @app.route( "/v2/assets//asset_type/", methods=["GET"] ) @json_schema.validate_headers(request, required=False) def get_v2_assets_info_by_asset_type(product_id: int, asset_type: str) -> Response: """Get s3 details for MP3_192/WAV/TIF asset_types. Args: product_id (int): url parameter that identifies product. asset_type (str): url parameter that identifies type of asset. Returns: Response: Response body with s3 details. """ if (asset_type := asset_type.upper()) not in asset_types.FINAL_ASSETS_ASSET_TYPES: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message="Unsupported Asset File Type", status=400, ) ) track_id = ( None if asset_type == asset_types.TYPE_FILE_TIF else request.args.get("track_id", None) ) return flaskify( ows_response.Response( asset_info.get_v2_assets_info_by_asset_type( product_id, asset_type, int(track_id) if track_id else None ) ) ) @app.route("/v2/asset", methods=["POST"]) @json_schema.validate_body(request, body.post_asset_schema_v2) @block_alw_and_oa_users def post_asset_handler_v2() -> Response: """Save info about uploaded asset from lambda. Returns: Response: Response containing upload data or error. """ data = request.get_json() # TODO: Once everyone is using the new asset upload flow this endpoint can be removed and replaced with a HEAD # endpoint that checks if there is an asset_upload record for a given filename. upc = data.get(field_const.UPC) if upc: upc = int(upc) return flaskify( ows_response.Response( asset_upload.update_asset_upload( filename=data[field_const.FILENAME], upc=upc, track_unique_id=data.get(field_const.TRACK_UNIQUE_ID), product_id=data.get(field_const.PRODUCT_ID), original_filename=data.get(field_const.ORIGINAL_FILENAME), is_correction=data.get(field_const.IS_CORRECTION, False), ) ) ) @app.route("/v2/asset/product//corrections/apply", methods=["PUT"]) @json_schema.validate_headers(request, required=False) def apply_asset_corrections(product_id: int) -> Response: """Set asset_upload's is_correction flag to false for a product. Args: product_id (int): url parameter that identifies product. Returns: Response: Response product body. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) return flaskify( ows_response.Response(asset_upload.apply_asset_corrections(int(product_id))) ) @app.route("/v2/asset/status/", methods=["GET"]) @json_schema.validate_headers(request, required=False) def get_asset_general_status_v2(filename: str) -> Response: """Return general status of asset processing. Args: filename (str): filename with extension. Returns: Response: Response containing success or error message. """ user_id = request.headers.get(field_const.ORCHARD_USER_ID) identity_id = request.headers.get(headers.ORCHARD_IDENTITY_ID) if not user_id and not handlers_utils.is_jwt_identity_authorized(identity_id): return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message="Failed to identify user", status=401, ) ) account_type = request.headers.get(field_const.GRASS_ACCOUNT_TYPE) account_id = request.headers.get(field_const.GRASS_ACCOUNT_ID) asset_upload_data = asset_upload.get_asset_upload_by_filename(filename) product_id = asset_upload_data["product_id"] if product_id and account_id is not None and account_type is not None: ownership_response = ownership.check_ownership( product_id=product_id, account_type=account_type, account_id=int(account_id) ) if ownership_response.status != ows_response.status.OK: return flaskify(ownership_response) return flaskify( ows_response.Response(asset_status.get_current_status(asset_upload_data["id"])), encoder=json_encoder.DatetimeEncoder, ) # /internal/ is configured in ows-grass to block all requests, meaning it can be accessed only directly @app.route("/internal/v2/asset//owner", methods=["GET"]) def get_asset_owner(filename: str) -> Response: """Return the owner of an asset identified by its filename. Resolves the asset's product id and queries ows-product for vendor and subaccount ownership ids. Args: filename (str): Filename of the asset (with or without extension). Returns: Response: Response containing ownership data or error message. """ return flaskify(ows_response.Response(ownership.get_asset_product_owner(filename))) @app.route("/internal/assets//stereo", methods=["GET"]) @handlers_utils.validate_jwt( [authorization.LAMBDA_ASSETS_SPATIAL_AUDIO_VALIDATION_UUID] ) def get_stereo_for_asset(asset_upload_filename: str) -> Response: return flaskify( ows_response.Response( stereo_reference.get_stereo_for_asset(asset_upload_filename) ) ) @app.route("/v2/asset/status", methods=["POST"]) @json_schema.validate_body(request, body.post_asset_status_schema) def post_asset_general_status_v2() -> Response: """Update general status of asset processing. Returns: Response: Response containing success or error message. """ payload = request.get_json() return flaskify( ows_response.Response( asset_status.create_status_by_filename( filename=payload.get(field_const.FILENAME), status=payload.get(field_const.STATUS), description=payload.get(field_const.DESCRIPTION), message=payload.get(field_const.MESSAGE), timestamp=payload.get(field_const.TIMESTAMP), ), ), encoder=json_encoder.DatetimeEncoder, ) @app.route("/v2/asset", methods=["GET"]) @json_schema.validate_query(request, query.get_asset_info_schema) def get_asset_info() -> Response: """Get general status of asset processing. Returns: Response: Response containing success or error message. """ # validate_query will raise an error if the query parameters are invalid. filename = request.args[field_const.FILENAME] state = request.args[field_const.STATE] return flaskify( ows_response.Response(asset_info.get_asset_info(filename=filename, state=state)) ) @app.route("/v2/asset/info", methods=["GET"]) @json_schema.validate_query(request, query.get_asset_info_by_id_schema) def get_asset_info_by_id() -> Response: """Get general status of asset processing. Returns: Response: Response containing success or error message. """ # validate_query will raise an error if the query parameters are invalid. asset_final_id = request.args[field_const.ASSET_FINAL_ID] return flaskify( ows_response.Response(asset_info.get_asset_info_by_id(int(asset_final_id))) ) @app.route("/v2/asset/download_url", methods=["GET"]) @handlers_utils.validate_jwt( [authorization.HIVE_AI_DETECTION_UUID, authorization.HIVE_TEXT_RECOGNITION_UUID] ) @json_schema.validate_query(request, query.get_asset_download_url_by_id_schema) def get_asset_download_url() -> Response: """Get presigned download url for asset_final_id. Returns: Response: Response containing presigned url or error. """ # validate_query will raise an error if the query parameters are invalid. asset_final_id = int(request.args[field_const.ASSET_FINAL_ID]) expiry = int(request.args.get(field_const.EXPIRES_IN, 900)) url = asset_info.get_asset_download_url_by_id( asset_final_id, expiry, ) return flaskify(ows_response.Response(url)) @app.route("/v2/asset/final", methods=["POST"]) @json_schema.validate_body(request, body.post_asset_final_status_schema) def post_asset_final_v2() -> Response: """Update encoding status and save info about final (encoded) asset. Returns: Response: Response containing success or error message. """ data = request.get_json() status = asset_status.map_encoding_state_to_status(data.get(field_const.STATUS)) asset_status.create_status_and_final_assets( filename=data.get(field_const.FILENAME), status=status, description=data.get(field_const.DESCRIPTION, ""), message=data.get(field_const.MESSAGE), timestamp=data.get(field_const.TIMESTAMP), final_assets=data.get(field_const.FINAL_ASSETS, []), ) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route("/asset/product/", methods=["GET"]) @app.route("/v2/asset/product/", methods=["GET"]) @json_schema.validate_headers(request, required=False) def get_product_by_id_v2(product_id: int) -> Response: """Return a set of general information for a particular release. Args: product_id (int): url parameter that identify product. Returns: Response: Response product body. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) product_assets_message = product_logic_v2.get_assets_by_product_id(int(product_id)) if not product_assets_message: return flaskify(ows_response.Response(product_assets_message)) # Assets in "uploading" state are dependent on the frontend completing the upload. # They are omitted from this response because only the frontend session performing # the upload should know about them. This prevents the frontend displaying abandoned # uploads. assets = [ asset for asset in product_assets_message["assets"] if asset["status"] != asset_status_constants.STATUS_UPLOADING ] return jsonify( { **product_assets_message, "assets": assets, } ) @app.route("/v2/asset/product//match-audio", methods=["GET"]) @json_schema.validate_headers(request, required=False) def get_content_review_match_audio(product_id: int) -> Response: """Get Audio Matches for Content Review. Retrieve results from a process that matches audio from asset_final records with publicly released audio. Args: product_id (int): path parameter that identifies a product. Returns: Response: List of tracks with audio check result. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) return flaskify( ows_response.Response( {"items": match_audio_logic.get_match_audio_results(product_id)} ) ) @app.route("/v2/asset/product//ai-generated-audio", methods=["GET"]) @json_schema.validate_headers(request, required=False) def get_content_review_ai_generated_audio(product_id: int) -> Response: """Get AI Audio Matches for Content Review. Retrieve results from a process that checks asset_final records for AI-generated audio with Hive AI detection. Args: product_id (int): path parameter that identifies a product. Returns: Response: List of tracks with AI generated audio results. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) return flaskify( ows_response.Response( { "items": ai_detection_logic.get_ai_generated_audio_results_by_product( product_id ) } ) ) # TODO: Delete this endpoint. It doesn't seem to be used. @app.route("/v2/image/", methods=["DELETE"]) @json_schema.validate_headers(request, required=False) def remove_product_image_v2(product_id: int) -> Response: """Remove a product image. Args: product_id (int): Unique identifier of product. Returns: Response: Response containing image removing status or error. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) return flaskify( ows_response.Response( product_logic_v2.remove_image_assets_by_product_id(product_id) ) ) @app.route("/v2/track/", methods=["DELETE"]) @json_schema.validate_headers(request, required=False) @json_schema.validate_query(request, query.delete_track_asset_schema) def remove_track_asset_v2(track_id: int) -> Response: """Remove assets for track with track_id. Args: track_id (int): Unique identifier of track. Returns: Response: Response containing track removing status or error. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_track_ownership, track_id ) if not ownership_result: return flaskify(ownership_result) asset_upload_type = request.args.get(field_const.ASSET_UPLOAD_TYPE) return flaskify( ows_response.Response(delete_logic_v2.delete_track(track_id, asset_upload_type)) ) @app.route("/v2/product/", methods=["DELETE"]) @json_schema.validate_headers(request, required=False) def remove_product_asset_v2(product_id: int) -> Response: """Remove assets for product with product_id. Args: product_id (int): Unique identifier of product. Returns: Response: Response containing track removing status or error. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) return flaskify(ows_response.Response(delete_logic_v2.delete_product(product_id))) @app.route("/v2/product//corrections", methods=["DELETE"]) @json_schema.validate_headers(request, required=False) def remove_product_asset_corrections_v2(product_id: int) -> Response: """Remove correction assets for product with product_id. Args: product_id (int): Unique identifier of product. Returns: Response: Response containing track removing status or error. """ return flaskify( ows_response.Response(delete_logic_v2.delete_product_corrections(product_id)) ) @app.route("/image////location", methods=["GET"]) def get_image_location(image_type: str, image_format: str, entity_id: str) -> Response: """Retrieve the location of an image from the cloudfront cdn. Args: image_type (str): Type of entity (e.g. "product" or "artist"). image_format (str): Whether this is for a thumbnail or cover. entity_id (str): Unique identifier for a product or artist. """ if image_type == "identity": # @todo replace this with wrapper when we have it. verify_headers = flask_request.verify_profile_headers(request) if not verify_headers: return flaskify(verify_headers) header_identity_id = request.headers.get(headers.ORCHARD_IDENTITY_ID) if header_identity_id and not header_identity_id == entity_id: return flaskify( ows_response.create_error_response( code=errors.UNAUTHORIZED_CODE, message="Request not allowed." ) ) # logo for User's identity is not yet implemented. So return 404. return flaskify( ows_response.create_not_found_response( message="No image for user's identity." ) ) # for backward compatibility with earlier endpoint talking int param. entity_id_int = int(entity_id) if image_type == "product": full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, entity_id_int ) if not ownership_result: return flaskify(ownership_result) if image_type == "product": return flaskify( ows_response.Response( image_location.get_image_location(entity_id_int, image_format) ) ) return flaskify( ows_response.Response( image_location_legacy.get_image_location( entity_id_int, image_type, image_format ) ) ) @app.route("/image/profile", methods=["GET"]) def get_profile_image() -> Response: """Retrieve the location of a user's profile image.""" request_context = context.get_request_context_from_headers( request.headers, label_profile=True ) profile_type = request_context.profile_type profile_id = request_context.profile_id return flaskify( ows_response.Response( image_location.get_profile_image(profile_id, profile_type) ) ) @app.route("/v2/image/product///location", methods=["GET"]) def get_image_location_v2(image_format: str, entity_id: str) -> Response: """Retrieve the location of an image from the cloudfront cdn. Args: image_format (str): Whether this is for a thumbnail or cover. entity_id (str): Unique identifier for a product or artist. """ # for backward compatibility with earlier endpoint talking int param. entity_id_int = int(entity_id) if user_util.is_workstation_user_request(request): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, entity_id_int ) if not ownership_result: return flaskify(ownership_result) force_uncorrected = request.args.get("force_uncorrected") == "true" return flaskify( ows_response.Response( image_location.get_image_location( entity_id_int, image_format, force_uncorrected ) ) ) @app.route("/image///location", methods=["GET"]) def get_image_locations(image_type: str, image_format: str) -> Response: """Retrieve locations of images in bulk from cloudfront CDN. Request args: ids: Comma-separated list of product or artist identifiers. Args: image_type (str): Type of entity (e.g. "product" or "artist"). image_format (str): Whether this is for a thumbnail or cover. Returns: Response: Image locations for the requested entities. """ ids = [int(x) for x in request.args.get("ids", "").split(",") if x] if image_type == "product": fallback = bool(int(request.args.get("fallback", "1"))) omit_corrections = request.args.get("omit_corrections") == "true" return flaskify( ows_response.Response( image_location.get_image_locations( ids, image_format, fallback, omit_corrections ) ) ) return flaskify( ows_response.Response( image_location_legacy.get_image_locations(ids, image_type, image_format) ) ) @app.route("/image///location", methods=["POST"]) @json_schema.validate_body(request, body.post_image_location_bulk) def post_image_locations(image_type: str, image_format: str) -> Response: """Retrieve locations of images in bulk from cloudfront CDN. Request body: entities: Array of entity objects with product_id (and optional upc). Args: image_type (str): Type of entity (e.g. "product" or "artist"). image_format (str): Whether this is for a thumbnail or cover. Returns: Response: Image locations for the requested entities. """ data = request.get_json() entities = data["entities"] if image_type == "product": fallback = bool(int(request.args.get("fallback", "1"))) omit_corrections = request.args.get("omit_corrections") == "true" return flaskify( ows_response.Response( image_location.get_image_locations( entities, image_format, fallback, omit_corrections ) ) ) # Extract entity_ids from entities for legacy endpoint entity_ids = [int(entity["product_id"]) for entity in entities] return flaskify( ows_response.Response( image_location_legacy.get_image_locations( entity_ids, image_type, image_format ) ) ) @app.route("/image//copy/", methods=["POST"]) @json_schema.validate_headers(request, required=False) def copy_product_artwork(from_pid: int, to_pid: int) -> Response: """Copy image assets from source product to destination product. Args: from_pid (int): Source product id. to_pid (int): Destination product id. Returns: Response: Response which contains copy process status or error. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if full_user_id is None: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message="Failed to identify user", status=401, ) ) asset_copy.copy_artwork_assets(int(from_pid), int(to_pid), full_user_id) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route("/stream/track//hls", methods=["GET"]) @json_schema.validate_headers(request, required=False) @json_schema.validate_query(request, query.get_hls_streaming_schema) def get_hls_streaming_link(track_id: int) -> Response: """Return an HLS URL for particular asset, which can be used for streaming. Args: track_id (int): query parameter that identify track. Returns: Response: Response stream url. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) profile_type = None identity_uuid = None if full_user_id is None: header_context = context.get_request_context_from_headers(request.headers) profile_type = header_context.profile_type identity_uuid = header_context.profile_uuid # Custom roles/permissions checks # should be replaced with access_rules.yml implementation if ( header_context.context_type != "profile" or profile_type != "ContentProfile" or "review_digital_audio" not in header_context.roles ): return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message="No explicit access policy found", status=401, ) ) profile_access_check = ownership.check_profile_track_access( profile_type, header_context.profile_uuid, track_id ) if not profile_access_check: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message="User lacks ownership", status=403, ) ) if ( full_user_id and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_track_ownership, track_id ) if not ownership_result: return flaskify(ownership_result) is_correction_mode = request.args.get(field_const.IS_CORRECTION, "False") == "True" ip_address = None if request.headers.get("X-Forwarded-For"): ip_address = request.headers.get("X-Forwarded-For") if request.headers.get("X-Real-IP"): ip_address = request.headers.get("X-Real-IP") return flaskify( ows_response.Response( stream_info.get_track_wowza_stream_url_with_correction_assets( track_id, full_user_id, ip_address, request.referrer, is_correction_mode, profile_type, identity_uuid, ) ) ) @app.route("/v2/validators/product//artwork", methods=["GET"]) def validate_product_artwork(product_id: int) -> Response: """Verify if artwork is present, valid, and completely ingested. Args: product_id (int): ID of the product to validate. Returns: Response: response for complete artwork (200) or not (404). """ ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) image_location.validate_product_artwork(product_id) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route("/v2/validators/product//audio", methods=["GET"]) def validate_product_audio(product_id: int) -> Response: """Verify if all audio is present, valid, and completely ingested. Args: product_id (int): ID of the product to validate. Returns: Response: response for complete audio (200) or not (404). """ return flaskify( ows_response.Response( {"errorMsgs": validators.validate_audio_assets(product_id)} ) ) @app.route("/upload-token//", methods=["GET"]) @json_schema.validate_headers(request, required=False) def get_upload_token_for_entity(entity: str, entity_id: int) -> Response: """Generate data required for vendor or artist assets upload. Args: entity (str): name of the entity(vendor/artist) that need access. entity_id (int): id of artist_info or vendor. Returns: Response: Response containing upload data or error. """ if entity not in [s3.VENDOR_ENTITY, s3.ARTIST_ENTITY]: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_INVALID_ENTITY_TYPE, message=error.ERROR_MESSAGE_INVALID_ENTITY_TYPE, status=ows_response.status.NOT_FOUND, ) ) if entity == s3.VENDOR_ENTITY: vendor_ownership = flask_request.verify_grass_ownership( request, ownership.check_vendor_ownership, entity_id ) if vendor_ownership.status != ows_response.status.OK: return flaskify(vendor_ownership) if entity == s3.ARTIST_ENTITY: artist_photo_id = request.args.get(field_const.ARTIST_PHOTO_ID) if not artist_photo_id: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_MISSING_ARTIST_PHOTO_ID, message=error.ERROR_MESSAGE_MISSING_ARTIST_PHOTO_ID, ) ) ownership_result = flask_request.verify_grass_ownership( request, ownership.is_valid_artist, entity_id ) if not ownership_result: return flaskify(ownership_result) entity_id = int(artist_photo_id) duration = int(request.args.get(field_const.DURATION, config.STS_TOKEN_DURATION)) if duration < config.MIN_STS_TOKEN_DURATION: duration = config.MIN_STS_TOKEN_DURATION return flaskify( ows_response.Response( generate_upload_data.get_entity_upload_permission( duration, entity, entity_id ) ) ) @app.route("/image/vendor//asset/", methods=["GET"]) def get_vendor_image(vendor_id: int, asset_id: int) -> Response: """Retrieve the location of a vendor icon image. Args: vendor_id (int): Unique identifier for a vendor. asset_id (int): Unique identifier for an image_asset. """ return flaskify( ows_response.Response( image_location_legacy.get_vendor_icon(vendor_id, asset_id) ) ) @app.route("/v2/assets-bulk", methods=["GET"]) @handlers_utils.validate_jwt([authorization.BULK_ASSET_DOWNLOAD_IDENTITY_UUID]) def get_assets_info_by_many_product_ids() -> Response: """Get all asset details for many products. Returns: Response: Response body with s3 details. """ try: asset_types_param = request.args.get("asset_types") asset_types_input = ( [x.upper() for x in asset_types_param.split(",")] if asset_types_param else [] ) validate( instance=asset_types_input, schema={ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", "uniqueItems": True, "items": { "type": "string", "enum": list(asset_types.FINAL_ASSETS_ASSET_TYPES), }, }, ) product_ids_param = request.args.get("product_ids") product_ids = ( [int(x) for x in product_ids_param.split(",")] if product_ids_param else [] ) validate( instance=product_ids, schema={ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", "uniqueItems": True, "minItems": 1, "maxItems": 185, "items": {"type": "integer", "minimum": 1}, }, ) except (ValueError, ValidationError) as ve: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_QUERY_VALIDATION, message=str(ve), ), ) return flaskify( ows_response.Response( product_logic_v2.get_assets_info_by_product_ids( product_ids, asset_types_input ) ), encoder=json_encoder.DatetimeDecimalEncoder, ) @app.route("/product//assets", methods=["GET"]) @json_schema.validate_headers(request, required=False) def get_assets_info_by_single_product_id(product_id: int) -> Response: """Get all asset details for product id. Args: product_id : url parameter that identifies product. Returns: Response: Response body with s3 details. """ return flaskify( ows_response.Response( product_logic_v2.get_assets_info_by_product_id(product_id), ), encoder=json_encoder.DatetimeDecimalEncoder, ) @app.route("/hive_segments", methods=["POST"]) @handlers_utils.validate_jwt([authorization.HIVE_AI_DETECTION_UUID]) @json_schema.validate_body(request, body.post_hive_segment_schema) def save_hive_segment_data() -> Response: """Save hive_segment data.""" # PP shadow auth (CDAM-4044): tenant-less check; always allows. pdp_auth.shadow_authorization(pdp_auth.ACTION_UPDATE_AI_DETECTION) payload = request.get_json() defaults = { field_const.MUBERT: None, field_const.MUSICGEN: None, field_const.RIFFUSION: None, field_const.UDIO: None, field_const.SUNO: None, field_const.STABLE_AUDIO: None, field_const.YUE: None, field_const.MINIMAX: None, field_const.MUREKA: None, field_const.ACE_STEP: None, field_const.DUOBAO: None, field_const.GOOGLE: None, field_const.HEARTMULA: None, field_const.LOUDLY: None, } payload["segments"] = [defaults | x for x in payload["segments"]] hive_segment.save_hive_segment_data(payload) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route( "/validators/product//desired-bit-depth/", methods=["GET"] ) def check_for_desired_bit_depth(product_id: int, bit_depth: int) -> Response: """To validate bit depth of assets for provided UPC. Args: product_id (int): product_id to validate bit depth value bit_depth (int): Desired bit-depth value """ validate_bit_depth_by_upc.check_for_desired_bit_depth(product_id, bit_depth) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route("/track//copy/", methods=["POST"]) @json_schema.validate_headers(request, required=False) def copy_track(from_tuid: int, to_tuid: int) -> Response: """Create a copy of the track assets. Args: from_tuid (int): Original track unique id. to_tuid (int): New track unique id. Returns: Response: Response containing copying status or error. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if full_user_id is None: return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message="Failed to identify user", status=401, ) ) if not input_value_validator.is_not_self_copy(from_tuid, to_tuid): return flaskify( ows_response.create_error_response( code=error.ERROR_CODE_SELF_COPY, message=error.ERROR_ASSET_SELF_COPY, status=400, ) ) if full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH: from_track_ownership = flask_request.verify_grass_ownership( request, ownership.check_track_ownership, from_tuid ) if not from_track_ownership: return flaskify(from_track_ownership) to_track_ownership = flask_request.verify_grass_ownership( request, ownership.check_track_ownership, to_tuid ) if not to_track_ownership: return flaskify(to_track_ownership) track.copy_track_asset(from_tuid, to_tuid, full_user_id) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route("/hive-text-recognition", methods=["POST"]) @handlers_utils.validate_jwt([authorization.HIVE_TEXT_RECOGNITION_UUID]) @json_schema.validate_body(request, body.post_hive_text_recognition_schema) def save_hive_text_recognition() -> Response: """Save hive text recognition data for an image asset. Store the text extracted by the Hive OCR model for a given asset_final_id. Returns: Response: Success status. """ payload = request.get_json() hive_text_recognition_logic.save_hive_text_recognition(payload) return flaskify(ows_response.Response({"status": error.SUCCESS_CODE})) @app.route("/v2/asset/product//image-text-extract", methods=["GET"]) @json_schema.validate_headers(request, required=False) def get_hive_text_recognition(product_id: int) -> Response: """Get Hive text recognition result for a product's image asset. Retrieve stored OCR text extracted from an image asset associated with the product. Args: product_id (int): path parameter that identifies a product. Returns: Response: Image text extraction result. """ full_user_id, auth_type = user_util.get_user_id_from_headers_or_body(request) if ( full_user_id is not None and full_user_id.startswith("alw:") and auth_type == user_util.HEADERS_AUTH ): ownership_result = flask_request.verify_grass_ownership( request, ownership.check_ownership, product_id ) if not ownership_result: return flaskify(ownership_result) return flaskify( ows_response.Response( { "result": hive_text_recognition_logic.get_hive_text_recognition_by_product( product_id ) } ) ) @app.route("/hive_ai_image_task", methods=["POST"]) @json_schema.validate_body(request, body.post_hive_ai_image_task_schema) def save_hive_ai_image_task_data() -> Response: """Save Hive AI Content. Returns: Response: Response containing success or error message. """ payload = request.get_json() return flaskify( ows_response.Response( hive_ai_image_task.save_hive_ai_image_task_data( asset_final_id=payload.get(field_const.ASSET_FINAL_ID), task_id=payload.get(field_const.TASK_ID), class_name=payload.get(field_const.CLASS_NAME), score_value=payload.get(field_const.SCORE_VALUE), ) ) )