first commit
This commit is contained in:
+187
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
''' ADI builder
|
||||||
|
author: Steven Kuterna steven.kuterna@devoteam.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
from lxml import etree as ET
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
loglevel= logging.WARNING
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(loglevel)
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(loglevel)
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
logger.addHandler(ch)
|
||||||
|
|
||||||
|
def insertMetadata(tree, config):
|
||||||
|
if config['Verb'] is None: config['Verb'] = ''
|
||||||
|
if config['CreateDate'] is None:
|
||||||
|
now = datetime.now()
|
||||||
|
date = now.strftime("%Y-%m-%d")
|
||||||
|
config['CreateDate'] = date
|
||||||
|
try:
|
||||||
|
attributes= { 'Asset_Class': 'package',
|
||||||
|
'Asset_ID': str(config['AssetId']),
|
||||||
|
'Asset_Name': str(config['AssetName']),
|
||||||
|
'Provider': str(config['Provider']),
|
||||||
|
'Provider_ID': str(config['ProviderId']),
|
||||||
|
'Product': 'MOD',
|
||||||
|
'Description': str(config['Description']),
|
||||||
|
'Creation_Date': str(config['CreateDate']),
|
||||||
|
'Verb': str(config['Verb']),
|
||||||
|
'Version_Major': str(config['VersionMajor']),
|
||||||
|
'Version_Minor': str(config['VersionMinor'])
|
||||||
|
}
|
||||||
|
except KeyError as e:
|
||||||
|
logger.error('failed to add Metadata AMS attribute '+str(e))
|
||||||
|
sys.exit(0)
|
||||||
|
ET.SubElement(tree, 'AMS', attributes)
|
||||||
|
|
||||||
|
|
||||||
|
def createAMSattributes(asset_type, metadata):
|
||||||
|
if metadata['Verb'] is None: metadata['Verb'] = ''
|
||||||
|
try:
|
||||||
|
ams_attributes= { 'Asset_Class': asset_type,
|
||||||
|
'Asset_ID': asset_type.upper() + str(metadata['AssetId']),
|
||||||
|
'Asset_Name': str(metadata['AssetName'] + ' ' + asset_type),
|
||||||
|
'Provider': str(metadata['Provider']),
|
||||||
|
'Provider_ID': str(metadata['ProviderId']),
|
||||||
|
'Product': 'MOD',
|
||||||
|
'Description': str(metadata['Description'] + ' ' + asset_type),
|
||||||
|
'Creation_Date': str(metadata['CreateDate']),
|
||||||
|
'Verb': str(metadata['Verb']),
|
||||||
|
'Version_Major': str(metadata['VersionMajor']),
|
||||||
|
'Version_Minor': str(metadata['VersionMinor'])
|
||||||
|
}
|
||||||
|
except KeyError as e:
|
||||||
|
logger.error('failed to add Metadata AMS attribute '+str(e))
|
||||||
|
sys.exit(0)
|
||||||
|
return ams_attributes
|
||||||
|
|
||||||
|
|
||||||
|
def addAsset(tree, asset_type, asset, metadata, output):
|
||||||
|
if not 'Content' in asset:
|
||||||
|
for subasset in asset:
|
||||||
|
addAsset(tree, asset_type, asset[subasset], metadata, output)
|
||||||
|
return
|
||||||
|
logger.info(f'insert asset: {asset}')
|
||||||
|
|
||||||
|
ams_attributes = createAMSattributes(asset_type, metadata)
|
||||||
|
assetTree = ET.SubElement(tree, 'Asset')
|
||||||
|
assetMetadata = ET.SubElement(assetTree, 'Metadata')
|
||||||
|
ET.SubElement(assetMetadata, 'AMS', ams_attributes)
|
||||||
|
ET.SubElement(assetMetadata, 'App_Data', {'App': 'MOD', 'Type': asset_type})
|
||||||
|
|
||||||
|
for element in asset:
|
||||||
|
if type(asset[element]) is dict:
|
||||||
|
for subelement in asset[element]:
|
||||||
|
if type(asset[element][subelement]) is dict:
|
||||||
|
for subsubelemment in asset[element][subelement]:
|
||||||
|
ET.SubElement(assetMetadata, 'App_Data', {'App': 'MOD', 'Name': subsubelemment, 'Value': str(asset[element][subelement][subsubelemment])})
|
||||||
|
else:
|
||||||
|
if asset[element][subelement] is None:
|
||||||
|
asset[element][subelement] = ''
|
||||||
|
ET.SubElement(assetMetadata, 'App_Data', {'App': 'MOD', 'Name': element, 'Value': str(asset[element][subelement])})
|
||||||
|
else:
|
||||||
|
# is it content? add it under the Asset level
|
||||||
|
if str(element).upper() == 'CONTENT_CHECKSUM' or str(element).upper() == 'CONTENT_FILESIZE':
|
||||||
|
continue
|
||||||
|
if str(element).upper() == 'CONTENT':
|
||||||
|
if not os.path.exists(args.input):
|
||||||
|
logger.error(f"content file {asset[element]} is not found, exiting...")
|
||||||
|
sys.exit(0)
|
||||||
|
ET.SubElement(assetTree, 'Content', {'Value': asset[element]})
|
||||||
|
checksum = hashlib.md5(open(asset[element],'rb').read()).hexdigest()
|
||||||
|
filesize = os.path.getsize(asset[element])
|
||||||
|
ET.SubElement(assetMetadata, 'App_Data', {'App': 'MOD', 'Name': 'Content_CheckSum', 'Value': str(checksum)})
|
||||||
|
ET.SubElement(assetMetadata, 'App_Data', {'App': 'MOD', 'Name': 'Content_Filesize', 'Value': str(filesize)})
|
||||||
|
shutil.copyfile(asset[element], f'{output}/{asset[element]}')
|
||||||
|
continue
|
||||||
|
# no content, add it under the Metadata level
|
||||||
|
if asset[element] is None:
|
||||||
|
asset[element] = ''
|
||||||
|
ET.SubElement(assetMetadata, 'App_Data', {'App': 'MOD', 'Name': element, 'Value': str(asset[element])})
|
||||||
|
|
||||||
|
|
||||||
|
def addTitle(tree, title, metadata):
|
||||||
|
logger.info(f'insert title: {title}')
|
||||||
|
ams_attributes = createAMSattributes('title', metadata)
|
||||||
|
titleTree = ET.SubElement(tree, 'Metadata')
|
||||||
|
ET.SubElement(titleTree, 'AMS', ams_attributes)
|
||||||
|
ET.SubElement(titleTree, 'App_Data', {'App': 'MOD', 'Type': 'title'})
|
||||||
|
|
||||||
|
for element in title:
|
||||||
|
# check if there are multiple elements available
|
||||||
|
if type(title[element]) == dict:
|
||||||
|
for subelement in title[element]:
|
||||||
|
# check if it contains multiple attributes like Language and a Value
|
||||||
|
if type(title[element][subelement]) == dict:
|
||||||
|
attributes = {'App': 'MOD', 'Name': element}
|
||||||
|
# if the attribute is the same as the element change it to 'Value'
|
||||||
|
for subsubelement in title[element][subelement]:
|
||||||
|
if subsubelement == element:
|
||||||
|
attributes.update({'Value': title[element][subelement][subsubelement]})
|
||||||
|
else:
|
||||||
|
# just add it to the attributes list
|
||||||
|
attributes.update({subsubelement: title[element][subelement][subsubelement]})
|
||||||
|
# add the multiattribute element to the tree
|
||||||
|
ET.SubElement(titleTree, 'App_Data', attributes)
|
||||||
|
continue
|
||||||
|
# single attribute, add to the tree
|
||||||
|
if title[element][subelement] is None:
|
||||||
|
title[element][subelement] = ''
|
||||||
|
ET.SubElement(titleTree, 'App_Data', {'App': 'MOD', 'Name': element, 'Value': title[element][subelement]})
|
||||||
|
else:
|
||||||
|
# single element, add to the tree
|
||||||
|
if title[element] is None:
|
||||||
|
title[element] = ''
|
||||||
|
ET.SubElement(titleTree, 'App_Data', {'App': 'MOD', 'Name': element, 'Value': str(title[element])})
|
||||||
|
|
||||||
|
|
||||||
|
def insertAssets(tree, metadata, assets, output):
|
||||||
|
for asset in assets:
|
||||||
|
if asset.upper() == 'TITLE':
|
||||||
|
addTitle(tree, assets[asset], metadata)
|
||||||
|
else:
|
||||||
|
addAsset(tree, asset, assets[asset], metadata, output)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__" :
|
||||||
|
argparser = argparse.ArgumentParser(description='Creates ADI from yaml...')
|
||||||
|
argparser.add_argument('--input', required=False, default="adi.yaml", help="yaml input to build adi.xml")
|
||||||
|
argparser.add_argument('--output', required=True, help="output directory for xml and content files")
|
||||||
|
args = argparser.parse_args()
|
||||||
|
|
||||||
|
logger.debug(f"generating adi based on {args.input}")
|
||||||
|
if not os.path.exists(args.input):
|
||||||
|
logger.error(f"file {args.input} is not found, exiting...")
|
||||||
|
sys.exit(0)
|
||||||
|
with open(args.input, 'r') as ymlfile:
|
||||||
|
cfg = yaml.load(ymlfile, Loader=yaml.FullLoader)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.mkdir(args.output)
|
||||||
|
except FileExistsError:
|
||||||
|
logger.warning(f'directory {args.output} already exists!')
|
||||||
|
root = ET.Element('ADI')
|
||||||
|
metadataTree = ET.SubElement(root, 'Metadata')
|
||||||
|
insertMetadata(metadataTree, cfg['metadata'])
|
||||||
|
assetTree = ET.SubElement(root, 'Asset')
|
||||||
|
insertAssets(assetTree, cfg['metadata'], cfg['asset'], args.output)
|
||||||
|
tree = ET.ElementTree(root)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree.write(f'{args.output}/adi.xml', encoding='utf-8', xml_declaration=True, pretty_print=True, doctype='<!DOCTYPE ADI PUBLIC "ADI" "ADI.DTD">')
|
||||||
|
except Exception as e:
|
||||||
|
print(str(e))
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#YAML file used as input for adi builder
|
||||||
|
#main structure:
|
||||||
|
#
|
||||||
|
#-metadata
|
||||||
|
# containing the package metadata
|
||||||
|
#-asset
|
||||||
|
# containing the collection of assets:
|
||||||
|
# - title asset metadata
|
||||||
|
# - movie asset metadata + content
|
||||||
|
# - still-image asset(box-cover, poster, image) AMS element metadata + content
|
||||||
|
# - preview asset AMS element metadata + content
|
||||||
|
#
|
||||||
|
# when adding more than 1 element use any random element name (indicated with arrows <-- in example below) eg:
|
||||||
|
#asset:
|
||||||
|
# title:
|
||||||
|
# DefaultTitle: This is the default title
|
||||||
|
# Summary_Long:
|
||||||
|
# 1:
|
||||||
|
# Language: NLD
|
||||||
|
# Summary_Short: Lange NLD samenvatting
|
||||||
|
# 2:
|
||||||
|
# Language: ENG
|
||||||
|
# Summary_Short: Long ENG summary
|
||||||
|
# image:
|
||||||
|
# 1: <--
|
||||||
|
# qualifier: ScreenGrab1
|
||||||
|
# 2: <--
|
||||||
|
# qualifier: HighResPortrait
|
||||||
|
#
|
||||||
|
# more info about ADI on the wiki:
|
||||||
|
# https://wikiprojects.upc.biz/display/VBOASP/ADI+to+TVA+mapping+overview
|
||||||
|
# https://wikiprojects.upc.biz/display/VBOASP/ADI+metadata+ingest+specification+including+Series+delivery+via+xml
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
AssetId : 20200827160000
|
||||||
|
AssetName : ADI minimal test
|
||||||
|
Provider : VodFeatureTeam
|
||||||
|
ProviderId: VODFT
|
||||||
|
Description: test with minimal adi
|
||||||
|
Verb:
|
||||||
|
CreateDate:
|
||||||
|
VersionMajor: 0
|
||||||
|
VersionMinor: 1
|
||||||
|
asset:
|
||||||
|
title:
|
||||||
|
Title: This is the title
|
||||||
|
Summary_Long:
|
||||||
|
1:
|
||||||
|
Language: NLD
|
||||||
|
Summary_Short: Lange NLD samenvatting
|
||||||
|
2:
|
||||||
|
Language: ENG
|
||||||
|
Summary_Short: Long ENG summary
|
||||||
|
Summary_Short:
|
||||||
|
1:
|
||||||
|
Language: NLD
|
||||||
|
Summary_Short: korte NLD samenvatting
|
||||||
|
2:
|
||||||
|
Language: ENG
|
||||||
|
Summary_Short: short ENG summary
|
||||||
|
Rating: 15
|
||||||
|
Advisories: Not rated
|
||||||
|
Run_Time: 01:23:45
|
||||||
|
Display_Run_Time: 01:24
|
||||||
|
Genre: 1/1
|
||||||
|
Category:
|
||||||
|
1: root/VodFT
|
||||||
|
Licensing_Window_Start: 2020-08-31T00:00:00
|
||||||
|
Licensing_Window_End: 2020-09-30T23:59:59
|
||||||
|
Suggested_Price: 2.99
|
||||||
|
|
||||||
|
movie:
|
||||||
|
Subtitle_Languages:
|
||||||
|
1: ENG
|
||||||
|
2: NLD
|
||||||
|
Content: video.ts
|
||||||
|
|
||||||
|
preview:
|
||||||
|
Content: video.ts
|
||||||
|
|
||||||
|
image:
|
||||||
|
1:
|
||||||
|
Image_Qualifier: HighResPortrait
|
||||||
|
Image_Aspect_Ratio: "480x720"
|
||||||
|
Content: highresportrait.jpg
|
||||||
Reference in New Issue
Block a user