#!/usr/bin/python
################################
##
## Transmogrifier: BrightCove
## A Final Cut Server import/export tool
##
## Written by: Beau Hunter beau@318.com
## 318 Inc 05/2009
##
##
## This class is a decedent of TransmogrifierObject, and provides no additional
## interfaces above what is provided via it's parent, TransmogrifyTargetObject.
## However, numerous methods have been overridden to provide BrightCove-specific
## Operations and XML output
##
## Transmogrifier is free software: you can redistribute it and/or modify
## it under the terms of version 3 the GNU General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## Transmogrifier is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Transmogrifier. If not, see <http://www.gnu.org/licenses/>.
##
##
#############################################################
import os, os.path, re, glob, shutil, sys, types, datetime, time
from ftplib import FTP
from fcsxml import FCSXMLField, FCSXMLObject
from transmogrifierTarget import TransmogrifierTargetObject, MediaFile
from xml.dom import minidom
version = ".91beta"
build = "2010040101"
## Date/time string used for reporting
currentTime = datetime.datetime.fromtimestamp(time.mktime(datetime.datetime.now().timetuple()))
[docs]class BrightCoveObject(TransmogrifierTargetObject):
"""Our main brightcove object, used for collecting media files, interpretting
FCS XML, writing brightcove compliant XML and uploading"""
refID = ""
publisherID = ""
frameHeight = ""
frameWidth = ""
debug = False
validActions = ['upload']
def __init__(self,entityID=0):
"""Our construct, instantiate our members"""
TransmogrifierTargetObject.__init__(self,entityID)
self.refID = ""
self.multipleBitRate = False
self.overwriteExistingFiles = True
self.serviceName = "BrightCove"
self.supportSubDirs = ["xmlin", "xmlout", "media", "media/500.25","media/700.5", "media/1100.5", "media/1500", "media/3000", "media/thumbs", "media/stills"]
self.neededAttributes = ["entityID", "approver", "title", "description", "publisherID", "ftpUser", "ftpPassword", "ftpHost"]
self.reqFCSFields = ["BrightCove Publish History","Publish to BrightCove","Published to BrightCove","Publishing Approver"]
self.validActions = ['upload']
[docs] def deleteSupportFiles(self):
"""Delete Registered support files (media and xml)"""
errorCode = ""
## Call our parent, which removes any standard loaded media or xml files
if not TransmogrifierTargetObject.deleteSupportFiles(self):
errorCode = 1
## Delete our BrightCove manifest.xml
if self.supportPath:
xmlOutPath = os.path.join(self.supportPath, "xmlout", "%s_manifest.xml" % self.refID)
if os.path.exists(xmlOutPath):
self.logger("Removing file at path: '%s'" % xmlOutPath, "detailed")
os.remove(xmlOutPath)
if errorCode:
return False
else:
return True
[docs] def loadConfiguration(self, parser):
"""Load from configuration file, expects a ConfigParser type object. If you subclass,
you should call this function. If we return false then you should abort. or do your own sanity checks"""
if not TransmogrifierTargetObject.loadConfiguration(self, parser):
return False;
try:
self.ftpHost = parser.get("BrightCove","host")
self.ftpUser = parser.get("BrightCove","username")
self.ftpPassword = parser.get("BrightCove","password")
self.publisherID = parser.get("BrightCove","publisherid")
self.multipleBitRate = parser.getboolean("BrightCove","multiplebitrate")
except:
self.logger("loadConfiguration() Problem loading configuration records, please double check your configuration","error")
return True
[docs] def setFCSXMLFile(self,filePath):
"""import FCS XML file and set relevant member vars"""
## Call our parent class, which does most of the work, imports common
## variables (stored in member vars)
errorCode = ""
if not TransmogrifierTargetObject.setFCSXMLFile(self, filePath):
errorCode = 1
## Generate our BrightCove refID
if not self.refID:
self.refID = re.sub(' ','_',re.sub(r'^(.*?)\..*$',r'\1',self.title))
if not errorCode:
return True
else:
return False
[docs] def upload(self, dirPath=""):
'''Uploads all relative assets to the configured ftpHost, also calls xmlOut and uploads the resulting file'''
theError = ""
## Sanity Checks
if not dirPath:
dirPath = self.supportPath
if not os.path.isdir(dirPath):
self.logger("brightcove_xml:upload() Directory does not exist:'%s'" % dirPath, "error")
return False
xmlOutPath = os.path.join(dirPath, "xmlout", "%s_manifest.xml" % self.refID)
if not self.xmlOut(xmlOutPath):
self.logger("upload() could not write XML, exiting", "error")
return False
## Establish our FTP upload command STOR overrides, STOU fails on existing object
if self.overwriteExistingFiles:
ftpCommand = "STOR"
else:
ftpCommand = "STOU"
## Sanity checks and then try our FTP connection
if not self.ftpHost or not self.ftpUser or not self.ftpPassword:
self.logger("upload() missing parameters, could not establish connection to FTP server!", "error")
try:
ftp = FTP(self.ftpHost, self.ftpUser, self.ftpPassword)
except:
self.logger("upload() failed to connect to FTP server", "error")
return False
## Iterate through our stored files (assembled by self.readMediaFiles)
if len(self.files) > 0:
for file in self.files.itervalues():
try:
if os.path.exists(file.path):
theFile = open(file.path, "r")
if not file.uploadFileName:
theFileName = file.fileName
else:
theFileName = "%s" % file.uploadFileName
self.logger("upload() uplaoding file: '%s' as '%s'" % (file.fileName, theFileName), "normal")
ftp.storbinary("%s %s" % (ftpCommand,theFileName), theFile)
theFile.close()
except:
theError = file.path,sys.exc_info()[0]
self.logger("upload() could not upload file: '%s' Error:\n%s" % (theError), "error")
## shutil.copy(file.path, dirPath)
##if not os.path.isfile (os.path.join(dirPath,file.fileName)):
## theError = "Couldn't copy file: '%s'" % file.path
## self.logger("upload() %s" % theError, "error")
try:
if os.path.exists(xmlOutPath):
theFile = open(xmlOutPath, "r")
self.logger("upload() uplaoding file: 'manifest.xml'", "normal")
ftp.storbinary("%s manifest.xml" % (ftpCommand), theFile)
theFile.close()
else:
self.logger("upload() failed to upload file: 'manifest.xml'", "error")
theError = self.lastError
except:
theError = xmlOutPath,sys.exc_info()[0]
self.logger("upload() could not upload file: '%s' Error:\n%s" % (theError), "error")
## Build our FCS object for reporting
if not self.fcsXMLOutObject:
self.fcsXMLOutObject = FCSXMLObject(self.entityID)
fcsXMLOut = self.fcsXMLOutObject
currentTime = datetime.datetime.fromtimestamp(time.mktime(datetime.datetime.now().timetuple()))
if not theError:
fcsXMLOut.setField(FCSXMLField("Published to %s" % self.serviceName, "true", "bool"))
fcsXMLOut.setField(FCSXMLField("Publish to %s" % self.serviceName, "false", "bool"))
fcsXMLOut.setField(FCSXMLField("Status", "Verify Publishing"))
self.appendFCSField("BrightCove Publish History","%s: Successfully published to %s." % (currentTime,self.serviceName))
self.logger("Successfully published assets to %s." % (self.serviceName))
return True
else:
self.appendHistory("%s: Failed to publish to %s. Please try again. Error:\n\t%s" % (currentTime,self.serviceName,self.lastError))
fcsXMLOut.setField(FCSXMLField("Publish to %s" % self.serviceName, "false", "bool"))
self.logger("Failed to publish all assets to %s." % (self.serviceName), "error")
return False
[docs] def xmlOut(self, filePath=""):
'''Output our BrightCove compliant XML, if we are passed a filePath, we
output to it. '''
## Sanity checks and variable initialization
theThumbFile = ""
theStillFile = ""
theVideoFullFile = ""
if not os.path.isdir(self.fcsXMLOutDir):
self.logger("fcsXMLOutDir: '%s' does not exist!" % self.fcsXMLOutDir, "error")
return False
if not len(self.files) > 0:
self.readMediaFiles()
if not len(self.files) > 0:
self.logger("No media files were found to upload!", "error")
return False
if not self.approver:
self.logger("No Approver specified!", "error")
return False
if not self.description:
self.logger("No Description Provided!", "error")
return False
if not self.publisherID:
self.logger("No PublisherID specified!", "error")
return False
if not self.emailToNotify:
self.logger("No notification email address specified!", "error")
return False
if not self.title:
self.logger("No title specified!", "error")
return False
if (filePath and (not os.path.exists(filePath) \
or (os.path.exists(filePath) and self.overwriteExistingFiles))
and os.path.isdir(os.path.dirname(filePath))) \
or not filePath :
## create our new xml doc, add our root FCS elements:
## <?xml version="1.0"?>
## <publisher-upload-manifest publisher-id=\"$PUBLISHER_ID\" preparer=\"$PREPARER\">
## <notify email=\"$EMAIL_TO_NOTIFY\" />
self.xmlObject = minidom.Document()
xmlDoc = self.xmlObject
manifestElement = xmlDoc.createElement("publisher-upload-manifest")
xmlDoc.appendChild(manifestElement)
manifestElement.setAttribute("publisher-id", self.publisherID)
manifestElement.setAttribute("preparer", self.approver)
manifestElement.setAttribute("report-success", "true")
notifyElement = xmlDoc.createElement("notify")
notifyElement.setAttribute("email", self.emailToNotify)
manifestElement.appendChild(notifyElement)
renditionReferences = [];
## And then our individual fields.
for file in self.files.itervalues():
if file.fileType == "video":
theAssetElement = xmlDoc.createElement("asset")
theAssetElement.setAttribute("type","%s" % file.bcType)
if file.bcType == "VIDEO_FULL" and not self.multipleBitRate:
theVideoFullFile = file
theAssetElement.setAttribute("hash-code",
"%s" % file.checksum)
theAssetElement.setAttribute("size", "%d" % file.size)
if self.multipleBitRate:
theAssetElement.setAttribute("frame-width",
"%d" % file.frameWidth)
theAssetElement.setAttribute("frame-height",
"%d" % file.frameHeight)
theAssetElement.setAttribute("h264-no-processing","true")
if file.bitRate:
theAssetElement.setAttribute("encoding-rate", "%d000"
% file.bitRate)
else:
theAssetElement.setAttribute("encode-to","MP4")
theAssetElement.setAttribute("encode-multiple","true")
if file.uploadFileName:
theAssetElement.setAttribute("filename", "%s" % file.uploadFileName)
else:
theAssetElement.setAttribute("filename", "%s" % file.fileName)
theAssetElement.setAttribute("refid","%s" % file.refID)
if self.multipleBitRate:
renditionReferences.append("%s" % file.refID)
## Append our field element to our "params" element i.e.
## <asset refid="FMX_Open_Full_4Mbps_24i" type="FLV_FULL" \
## hash-code="f0e24166abdf5e542c3c6427738bba8f" size="38218785"\
## filename="FMX_Open_Full_4Mbps_24i.mp4" encoding-rate="3700670"\
## frame-width="640" frame-height="480"/>
elif file.fileType == "image":
if file.bcType == "THUMBNAIL":
## Generate our thumbnail
theThumbFile = file
elif file.bcType == "VIDEO_STILL":
## Generate our video still
theStillFile = file
theAssetElement = xmlDoc.createElement("asset")
theAssetElement.setAttribute("refid","%s" % file.refID)
theAssetElement.setAttribute("filename","%s" % file.uploadFileName)
theAssetElement.setAttribute("type","%s" % file.bcType)
theAssetElement.setAttribute("hash-code","%s" % file.checksum)
theAssetElement.setAttribute("size", "%d" % file.size)
theAssetElement.setAttribute("frame-width", "%d" % file.frameWidth)
theAssetElement.setAttribute("frame-height", "%d" % file.frameHeight)
else:
self.logger("Unknown media type: '%s' for file: '%s'" % (file.fileType, file.path))
return False;
manifestElement.appendChild(theAssetElement)
del theAssetElement
## Append our title element
titleElement = xmlDoc.createElement("title")
titleElement.setAttribute("name", "%s" % self.title)
titleElement.setAttribute("refid", "%s_title" % self.refID)
titleElement.setAttribute("active", "true")
if theThumbFile:
titleElement.setAttribute("thumbnail-refid", "%s" % theThumbFile.refID)
if theStillFile:
titleElement.setAttribute("video-still-refid", "%s" % theStillFile.refID)
if theVideoFullFile:
titleElement.setAttribute("video-full-refid","%s" % theVideoFullFile.refID)
manifestElement.appendChild(titleElement)
descElement = xmlDoc.createElement("short-description")
if self.description:
theValueNode = xmlDoc.createTextNode("%s" % self.description)
else:
theValueNode = xmlDoc.createTextNode(" ")
descElement.appendChild(theValueNode)
titleElement.appendChild(descElement)
if self.longDescription:
ldescElement = xmlDoc.createElement("long-description")
theValueNode = xmlDoc.createTextNode("%s" % self.longDescription)
ldescElement.appendChild(theValueNode)
titleElement.appendChild(ldescElement)
if self.keywordString:
for tag in self.keywordString.split(","):
tagElement = xmlDoc.createElement("tag")
theValueNode = xmlDoc.createTextNode("%s" % tag)
tagElement.appendChild(theValueNode)
titleElement.appendChild(tagElement)
for item in renditionReferences[:]:
renditionRefElement = xmlDoc.createElement("rendition-refid")
theValueNode = xmlDoc.createTextNode(item)
renditionRefElement.appendChild(theValueNode)
titleElement.appendChild(renditionRefElement)
del renditionRefElement
if filePath:
theFile = open(filePath, "w")
xmlDoc.writexml(theFile)
theFile.close()
else:
print xmlDoc.toprettyxml()
elif os.path.exists(filePath) and not self.overwriteExistingFiles:
self.logger("File already exists at path: %s, exiting!" % filePath, "error")
return False
elif not os.path.exists(os.path.dirname(filePath)):
self.logger("Directory does not exist at path: %s, exiting!" % os.path.dirname(filePath), "error")
return false
else:
self.logger("Uncaught Exception: Error writing XML", "error")
return False
xmlDoc.unlink()
return True