diff --git a/Python-Version/scripts/a2_to_a3_upload_polling.py b/Python-Version/scripts/a2_to_a3_upload_polling.py index fd80c3d..7aa2de5 100755 --- a/Python-Version/scripts/a2_to_a3_upload_polling.py +++ b/Python-Version/scripts/a2_to_a3_upload_polling.py @@ -254,7 +254,19 @@ def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config, not # 5. Get clean filename clean_filename = parser.strip_upload_components(filename) - # 6. Build MVP asset representation with CreativeX data from database + # 6. Look up pre-upload metadata override saved by the naming tool's editor. + # The naming tool stores filename without extension, so strip it here. + filename_no_ext = os.path.splitext(filename)[0] + override = db.get_override_metadata(filename_no_ext) + override_fields = None + if override: + override_fields = override.get('override_fields') + logger.info("Found pre-upload override (id={}) for {}: {} field(s)".format( + override.get('id'), filename_no_ext, + len(override_fields) if override_fields else 0 + )) + + # 7. Build MVP asset representation with CreativeX data from database asset_rep = mvp_extractor.build_mvp_asset_representation( master_metadata=master_asset['full_metadata'], clean_filename=clean_filename, @@ -262,7 +274,8 @@ def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config, not box_metadata=box_metadata, # Pass CreativeX data from database tracking_mode=tracking_mode, # Pass tracking mode for folder-only handling master_opentext_id=master_asset['opentext_id'], # Primary master DAM ID - master_opentext_ids=master_opentext_ids # All master IDs (multiple or single) + master_opentext_ids=master_opentext_ids, # All master IDs (multiple or single) + override_fields=override_fields # Pre-upload edits from naming tool ) # DRYRUN MODE: Display full asset representation and exit @@ -355,6 +368,10 @@ def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config, not filename=clean_filename ) + # Mark pre-upload override as applied (only after confirmed DAM upload success). + if override: + db.mark_override_applied(filename_no_ext) + # 9. Delete file from Box after successful upload (unless --keep-files flag set) if keep_files: logger.info("--keep-files flag set - File kept in Box: {}".format(filename)) diff --git a/Python-Version/scripts/shared/database.py b/Python-Version/scripts/shared/database.py index a836d0f..9e9d208 100644 --- a/Python-Version/scripts/shared/database.py +++ b/Python-Version/scripts/shared/database.py @@ -1104,6 +1104,88 @@ class Database: cursor.close() self.put_connection(conn) + def get_override_metadata(self, filename_without_ext): + """ + Look up pre-upload metadata override saved by the naming tool. + + Returns the latest unapplied override row for this filename, or None. + If the override_metadata table doesn't exist (e.g., on a dev DB where the + naming tool migration hasn't been run), returns None — upload behaviour + falls back to today's defaults. + """ + conn = self.get_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + SELECT id, tracking_id, override_fields + FROM override_metadata + WHERE filename = %s + AND applied_to_upload = FALSE + ORDER BY created_at DESC + LIMIT 1 + """, (filename_without_ext,)) + + row = cursor.fetchone() + if not row: + return None + + override_fields = row[2] if isinstance(row[2], dict) else json.loads(row[2]) + return { + 'id': row[0], + 'tracking_id': row[1], + 'override_fields': override_fields, + } + except psycopg2.errors.UndefinedTable: + conn.rollback() + logger.warning("override_metadata table does not exist - skipping override lookup") + return None + except Exception as e: + conn.rollback() + logger.error("Failed to query override_metadata for '{}': {}".format( + filename_without_ext, str(e) + )) + return None + finally: + cursor.close() + self.put_connection(conn) + + def mark_override_applied(self, filename_without_ext): + """ + Mark a pre-upload override row as applied after a successful DAM upload. + Only updates rows that are currently applied_to_upload = FALSE. + """ + conn = self.get_connection() + try: + cursor = conn.cursor() + cursor.execute(""" + UPDATE override_metadata + SET applied_to_upload = TRUE, + applied_at = CURRENT_TIMESTAMP + WHERE filename = %s + AND applied_to_upload = FALSE + """, (filename_without_ext,)) + + updated = cursor.rowcount + conn.commit() + + if updated: + logger.info("Marked {} override row(s) as applied for '{}'".format( + updated, filename_without_ext + )) + return updated + except psycopg2.errors.UndefinedTable: + conn.rollback() + return 0 + except Exception as e: + conn.rollback() + logger.error("Failed to mark override applied for '{}': {}".format( + filename_without_ext, str(e) + )) + return 0 + finally: + cursor.close() + self.put_connection(conn) + def close(self): """Close all connections in pool""" if self.pool: diff --git a/Python-Version/scripts/shared/metadata_extractor_mvp.py b/Python-Version/scripts/shared/metadata_extractor_mvp.py index a822865..2acbbf0 100644 --- a/Python-Version/scripts/shared/metadata_extractor_mvp.py +++ b/Python-Version/scripts/shared/metadata_extractor_mvp.py @@ -13,6 +13,36 @@ from shared.config_loader import load_country_code_mappings logger = logging.getLogger('MetadataExtractorMVP') +# Editor field name -> DAM metadata field ID. +# Mirrors the canonical mapping in the naming tool's public-v2/Database.php +# so that pre-upload overrides saved via the metadata editor are applied to +# the matching DAM fields on upload. +OVERRIDE_FIELD_MAP = { + 'validity_start': 'FERRERO.FIELD.ASSET VALIDITY START PERIOD', + 'validity_end': 'FERRERO.FIELD.ASSET VALIDITY END PERIOD', + 'marketing_tag': 'MARKETING_TAG', + 'agency_name': 'FERRERO.MARKETING.FIELD.AGENCY NAME', + 'spot_version': 'FERRERO.MARKETING.FIELD.SPOT_VERSION', + 'director_name': 'FERRERO.MARKETING.FIELD.DIRECTOR_NAME', + 'video_post_prod_company': 'FERRERO.MARKETING.FIELD.VIDEO_POST_PROD_COMPANY', + 'video_post_prod_contact': 'FERRERO.MARKETING.FIELD.VID_POST_PROD_CONTACT', + 'audio_post_prod_company': 'FERRERO.MARKETING.FIELD.AUDIO_POST_PROD_COMPANY', + 'audio_post_prod_contact': 'FERRERO.MARKETING.FIELD.AUDIO_POST_PROD_CONTACT', + 'video_type': 'FERRERO.MARKET.FIELD.TYPE_VID', + 'ip_rights': 'FERRERO.MARKET.FIELD.IPRIGHT', + 'production_company': 'FERRERO.MARKET.PROD_COMPANY', + 'licensing': 'FERRERO.MARKET.FIELD.LICENSIN', + 'buyout': 'FERRERO.MARKET.FIELD.BUYOUT', + 'ferrero_property': 'FERRERO.MARKET.FIELD.FERRERO PROPERTY', + 'video_status': 'FERRERO.MARKET.VID_N_STAT', + 'license': 'FERRERO.MARKET.FIELD.LICENSE', + 'creativex_score': 'FERRERO.TAB.FIELD.CREATIVEX', + 'creativex_link': 'FERRERO.FIELD.CREATIVEX LINK', +} + +DATE_OVERRIDE_FIELDS = {'validity_start', 'validity_end'} + + class MetadataExtractorMVP: def __init__(self, field_mappings): """ @@ -113,7 +143,7 @@ class MetadataExtractorMVP: return extracted_fields - def build_mvp_asset_representation(self, master_metadata, clean_filename, parsed_filename, box_metadata=None, tracking_mode='full', master_opentext_id=None, master_opentext_ids=None): + def build_mvp_asset_representation(self, master_metadata, clean_filename, parsed_filename, box_metadata=None, tracking_mode='full', master_opentext_id=None, master_opentext_ids=None, override_fields=None): """ Build asset representation with MVP fields + updates from filename @@ -124,6 +154,10 @@ class MetadataExtractorMVP: box_metadata: Optional Box metadata tracking_mode: 'full' (inherit all metadata) or 'folder_only' (only use folder) master_opentext_id: Optional DAM Asset ID of master asset (for derivative tracking) + override_fields: Optional dict of pre-upload metadata overrides keyed by + editor field name (e.g. {'validity_end': '...', 'ip_rights': 'Yes'}). + Applied after master/filename/forced values but before asset-type + overrides so EOL/LTD compliance still wins. Empty values are skipped. Returns: Asset representation dict ready for upload @@ -160,6 +194,13 @@ class MetadataExtractorMVP: if box_metadata: mvp_fields = self._update_creativex_fields(mvp_fields, box_metadata) + # Apply pre-upload metadata overrides from the naming tool's editor. + # Runs after master/filename/forced/default/CreativeX values so it wins + # over them, but before asset_type_overrides so EOL/LTD compliance rules + # still take final precedence. + if override_fields: + mvp_fields = self._apply_override_fields(mvp_fields, override_fields) + # Apply asset type overrides (e.g., EOL, LTD) - takes final precedence over # forced values, defaults, and CreativeX (LTD removes CreativeX entirely). mvp_fields = self._apply_asset_type_overrides(mvp_fields, parsed_filename) @@ -921,6 +962,72 @@ class MetadataExtractorMVP: return field['value']['value']['field_value'].get('value') return None + def _apply_override_fields(self, mvp_fields, override_fields): + """ + Apply pre-upload metadata overrides from the naming tool. + + For each non-empty entry in override_fields, map the editor field name + to its DAM field ID via OVERRIDE_FIELD_MAP and write the value into the + matching field in mvp_fields. Empty strings are skipped (treat as + "user didn't set this, leave inherited value alone"). Validity dates + from the editor arrive as ISO 8601 strings and are normalised to the + MM/DD/YYYY format DAM expects. + """ + if not override_fields: + return mvp_fields + + applied = 0 + for editor_field, raw_value in override_fields.items(): + if raw_value is None or raw_value == '': + continue + + dam_field_id = OVERRIDE_FIELD_MAP.get(editor_field) + if not dam_field_id: + logger.debug("Override: no DAM mapping for editor field '{}' - skipping".format(editor_field)) + continue + + value = raw_value + if editor_field in DATE_OVERRIDE_FIELDS: + value = self._normalize_iso_date(raw_value) + if not value: + continue + + target = None + for field in mvp_fields: + if field.get('id') == dam_field_id: + target = field + break + + if target is None: + logger.warning("Override: field {} (DAM id {}) not present in mvp_fields - skipping".format( + editor_field, dam_field_id + )) + continue + + if editor_field in DATE_OVERRIDE_FIELDS: + self._set_date_field_value(target, value) + else: + self._set_field_value(target, value) + + logger.info("Override applied: {} ({}) = {}".format(editor_field, dam_field_id, value)) + applied += 1 + + if applied: + logger.info("Applied {} pre-upload override field(s) from naming tool".format(applied)) + return mvp_fields + + def _normalize_iso_date(self, iso_str): + """Convert an ISO 8601 date string (with or without time/timezone) to MM/DD/YYYY.""" + if not iso_str: + return None + try: + date_part = iso_str.split('T')[0] + dt = datetime.strptime(date_part, '%Y-%m-%d') + return dt.strftime('%m/%d/%Y') + except Exception as e: + logger.warning("Could not normalize override date '{}': {}".format(iso_str, str(e))) + return None + def _set_field_value(self, field, value): """Set field value handling different structures""" import json