Source code for shakeblender

#!/usr/bin/python
# -*- coding: utf-8 -*-


################################
##
##  Transmogrifier: ShakeBlender
##  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 shake-specific
##  Image manipulation operations.
##
##  Copyright © 2009-2011 Beau Hunter, 318 Inc.
##
##  This file is part of Transmogrifier.
##
##  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, tempfile
from ftplib import FTP
from fcsxml import FCSXMLField, FCSXMLObject
from transmogrifierTarget import TransmogrifierTargetObject, MediaFile
from ConfigParser import *
import subprocess 


from xml.dom import minidom
    
    
## Date/time string used for reporting
currentTime = datetime.datetime.fromtimestamp(time.mktime(datetime.datetime.now().timetuple()))

[docs]class ShakeBlenderObject(TransmogrifierTargetObject): """Our main shakeBlender object, used for processing media files with Shake""" bgFilePath = "" backgroundSource = "" inFilePath = "" outFilePath = "" validActions = ['autoKey'] debug = False def __init__(self,entityID=0): """Our construct, instantiate our members""" TransmogrifierTargetObject.__init__(self,entityID) self.refID = "" self.multipleBitRate = True self.overwriteExistingFiles = True self.serviceName = "ShakeBlender" self.backgroundSource = "" self.supportSubDirs = ["xmlin","background","support","support/tmp","out_videocrew1","out_videocrew2"] self.neededAttributes = ["title","backgroundSource"] self.validActions = ['autoKey']
[docs] def setFCSXMLFile(self,filePath): """import FCS XML file, and set applicable local values""" if not TransmogrifierTargetObject.setFCSXMLFile(self, filePath): self.logger("setFCSXMLFile() parent could not load file, exiting","error") return False; device = self.fcsXMLObject.valueForField("Stored On") deviceRelPath = self.fcsXMLObject.valueForField("Location") fileName = self.fcsXMLObject.valueForField("File Name") devicePath = self.pathForDevice(device) if not devicePath: self.logger("setFCSXMLFile() could not determine path for device:'%s'," % device,"error") inFilePath = filePath else: ## make sure we get rid of the leading / on our deviceRelPath inFilePath = os.path.join(devicePath,deviceRelPath[1:],fileName) ## try to read the background Source from the filename. fileNameReObj = re.match("(.*)(\d{2})\.mov",fileName) if fileNameReObj: fileName = "%s.mov" % fileNameReObj.group(1) backgroundCode = int(fileNameReObj.group(2)) if backgroundCode == 1: backgroundSource = "snowmen" elif backgroundCode == 2: backgroundSource = "ornaments" elif backgroundCode == 3: backgroundSource = "hills" if not backgroundSource: self.logger("setFCSXMLFile() determining background source from XML!") backgroundSource = self.fcsXMLObject.valueForField("Background Source") try: videoCrewNum = int(self.fcsXMLObject.valueForField("Video Crew")) except: self.logger("setFCSXMLFile() Error reading field \"Video Crew\"","error") videoCrewNum = 1 if not isinstance(self.configParser,ConfigParser): self.logger("setFCSXMLFile() No valid ConfigParser object loaded!", "error") return False ## Get our background file path from settings. Looks for config key ## with the name stored in var bgKey try: parser = self.configParser bgKey = "%s_backgroundfile" % backgroundSource bgFilePath = parser.get("SHAKE_BLENDER",bgKey) except: self.logger("setFCSXMLFile() No background defined for source:'%s' at key:'%s' in config file, attempting to use defaults" % (backgroundSource,bgKey), "detailed") try: bgFilePath = parser.get("SHAKE_BLENDER","default_backgroundfile") except: self.logger("setFCSXMLFile() Could determine background for source:'%s'" % backgroundSource, "error") return False ## Get our outfile path try: outFileKey = "%s_outpath" % backgroundSource outFilePath = parser.get("SHAKE_BLENDER",outFileKey) except: self.logger("setFCSXMLFile() No outpath defined for source:'%s' at key:'%s' in config file, attempting to use defaults" % (backgroundSource,outFileKey), "detailed") try: outFilePathConf = parser.get("SHAKE_BLENDER","default_outpath") outFilePath = os.path.join(outFilePathConf,"out_videocrew%s" % videoCrewNum,backgroundSource) except: outFilePath = os.path.join(self.supportPath,"out_videocrew%s" % videoCrewNum,backgroundSource) self.logger("setFCSXMLFile() Determined final outFilePath to be:'%s'" % outFilePath, "detailed") if not os.path.isdir(outFilePath): if not os.makedirs(outFilePath): self.logger("setFCSXMLFile() Output directory:'%s' for background source:'%s' does not exist, make sure that '%s_outpath' is specified in your config file!" %(outFilePath,backgroundSource,backgroundSource), "error") return False ##self.outFilePath = os.path.join(outFilePath,"%s_%s.mov" % (self.title,backgroundSource)) self.outFilePath = os.path.join(outFilePath,fileName) self.logger("setFCSXMLFile() Determined final output file to be:'%s'" % self.outFilePath, "detailed") if os.path.isfile(bgFilePath): self.bgFilePath = bgFilePath else: self.logger("setFCSXMLFile() Could not find file:'%s' for background source: %s, make sure that %s_backgroundfile is specified in your config file!" %(bgFilePath,backgroundSource,backgroundSource), "error") return False if os.path.isfile(inFilePath): self.inFilePath = inFilePath else: self.logger("setFCSXMLFile() Could not find input file for processing at path:'%s'" % inFilePath, "error") return False return True
def frameCountForInFile(self): if os.path.isfile(self.inFilePath): frameCount = self.frameCountForMovieAtPath(self.inFilePath) if not frameCount: self.logger("frameCountForInFile() could not determine framecount from file at path:'%s', reading from XML!" % self.inFilePath,"error") frameRate = float(self.fcsXMLObject.valueForField("Video Frame Rate")); duration = float(self.fcsXMLObject.valueForField("Duration")); frameCount = duration * frameRate self.logger("frameCountForInFile() determined framecount: '%s' from XML. frameRate:'%s' duration:'%s'" % (frameCount,frameRate,duration),"error") return frameCount def frameCountForBgFile(self): if os.path.isfile(self.bgFilePath): frameCount = self.frameCountForMovieAtPath(self.bgFilePath) if not frameCount: ## hack for webeye, 10.5 can't pull framecount from qtinfo ## so we have to statically set this. bgSource = self.fcsXMLObject.valueForField("Background Source") if bgSource == "ornaments": frameCount = 572 elif bgSource == "snowmen": frameCount = 326 else: frameCount = 404 self.logger("frameCountForBgFile() could not determine framecount from file at path:'%s', using static value:'%s'" % (self.bgFilePath,frameCount),"error") return frameCount def frameCountForMovieAtPath(self, filePath): if not os.path.isfile(filePath): self.logger("frameCountForMovieAtPath() could not find movie at path:'%s'" % filePath,"error") return False ## this could be Pythonized quite a bit durationCMD = subprocess.Popen('/usr/libexec/podcastproducer/qtinfo "%s" | awk -F= \'/duration/ {print$2}\' | perl -p -e \'s/.*?\"(.*?)\".*$/$1/g\'' % filePath,shell=True,stdout=subprocess.PIPE,universal_newlines=True) frameRateCMD = subprocess.Popen('/usr/libexec/podcastproducer/qtinfo "%s" | awk -F= \'/frameRate/ {print$2}\' | perl -p -e \'s/(.*?);.*$/$1/g\'' % filePath,shell=True,stdout=subprocess.PIPE,universal_newlines=True) durationCMD_STDOUT, durationCMD_STDERR = durationCMD.communicate() frameRateCMD_STDOUT, frameRateCMD_STDERR = frameRateCMD.communicate() if not durationCMD_STDOUT: self.logger("frameCountForMovieAtPath() could not get duration for movie at path:'%s'" % filePath,"error") return False if not frameRateCMD_STDOUT: self.logger("frameCountForMovieAtPath() could not get framerate for movie at path:'%s'" % filePath,"error") return False ## get our duration and our framerate. Our duration should always ## be a float, our framerate can be an int or float duration = float(durationCMD_STDOUT) testNum = frameRateCMD_STDOUT.replace("\n","").replace(" ","").replace('"',"") try: frameRate = float(testNum) except: frameRate = int(frameRateCMD_STDOUT) frameCount = duration * frameRate self.logger("frameCountForMovieAtPath() path:'%s' frameCount:'%s' Duration:'%s' frameRate:'%s'" % (filePath,frameCount,duration,frameRate)) if int(round(frameCount)) < frameCount: return int(round(frameCount)) + 1 else: return int(round(frameCount))
[docs] def runFunction(self, function): """Performs Shake functions""" bgFilePath = self.bgFilePath if self.bgFilePath and os.path.isfile(self.bgFilePath): bgFilePath = self.bgFilePath elif self.bgFilePath: self.logger("specialFunction() bfFilePath:'%s' is not a file, cannot continue!" % self.bgFilePath,"error") return False else: self.logger("specialFunction() background file could not be determined, cannot continue!" % self.bgFilePath,"error") return False if function == "autoKey": ## get some information from our movie file supportDir = os.path.join(self.supportPath,"support") templateFilePath = os.path.join(self.supportPath,"support","templateScript.shk") ## make sure that we have a support dir and template file if os.path.isdir(supportDir): if os.path.isfile(templateFilePath): templateFileHandler = open(templateFilePath) else: self.logger("specialFunction('autoKey') Could not find template shake file at path: '%s' for processing!" % templateFilePath, "error") return False else: self.logger("specialFunction('autoKey') Could not find shakeblender support directory:'%s'" %supportDir, "error") return False tmpPath = os.path.join(self.supportPath,"support","tmp") if not os.path.exists(tmpPath): os.makedirs(tmpPath) tempDir = tempfile.mkdtemp(prefix=os.path.join(tmpPath,"%s_" % self.title)) tempAudioFilePath = os.path.join(tempDir,"audiofile.mov") tempRenderFilePath = os.path.join(tempDir,"renderfile.mov") tempReferenceFilePath = os.path.join(tempDir,"referencefile.mov") ## if we reach this point we have an open file handler in templateFileHandler ## create a temp file which will serve as our active shake script tempFileDescriptor,tempFilePath = tempfile.mkstemp(prefix="%s_" % self.title,suffix=".shk",dir=tempDir) tempFileHandler = os.fdopen(tempFileDescriptor, "w+") if tempFileHandler: ## iterate through our template file, write it to our tempfile ## replace our template file paths frameCount = self.frameCountForInFile() bgFrameCount = self.frameCountForBgFile() if not frameCount: self.logger("specialFunction() autoKey: Could not determine frame count for main clip, cannot continue!","error") return False if not bgFrameCount: self.logger("specialFunction() autoKey: Problem determining frame count for background, returning '%s'" % frameCount,"warning") self.bgFrameCount = frameCount while True: line = templateFileHandler.readline() if not line: break; line = line.replace('SetTimeRange("1")','SetTimeRange("1-%d")' % frameCount); line = line.replace("filein_background.mov",self.bgFilePath) line = line.replace("filein_greenscreen.mov",self.inFilePath) line = line.replace("fileout_movie.mov",tempRenderFilePath) line = line.replace('1000, "Freeze"','%s, "Freeze"' % (int(frameCount) + 1)) tempFileHandler.write(line) templateFileHandler.close() tempFileHandler.close() print "tempfilepath: %s" % tempFilePath ## Run our shake script shakeRetCode = subprocess.call("/usr/bin/shake -exec '%s'" % (tempFilePath),shell=True,universal_newlines=True) if shakeRetCode is not 0: self.logger("specialFunction('autoKey') Shake processing failed return code:'%s'" % shakeRetCode, "error") return False ## Extract the audio from our source file qtextractRetCode = subprocess.call("/usr/libexec/podcastproducer/qttrackextract audio '%s' '%s'" % (self.inFilePath,tempAudioFilePath),shell=True,stdout=subprocess.PIPE,universal_newlines=True) if qtextractRetCode is not 0: self.logger("specialFunction('autoKey') Could not extract audio from movie:'%s' return code:'%s'" %(self.inFilePath,qtextractRetCode), "error") return False ## Reattach the file qtTrackAddRetCode = subprocess.call("/usr/libexec/podcastproducer/qttrackadd '%s' '%s' '%s'" % (tempAudioFilePath,tempRenderFilePath,tempReferenceFilePath),shell=True,universal_newlines=True) ## "/usr/libexec/podcastproducer/qtjoin qttrackadd /private/tmp/audiofile.mov '" + self.outFilePath + "' '" + self.outFilePathStitched" if qtTrackAddRetCode is not 0: self.logger("specialFunction('autoKey') Could not place audiofile:'%s' into movie:'%s' return code:'%s'" %(tempAudioFilePath,tempRenderFilePath,qtextractRetCode), "error") return False ## Save a flattened copy of the movie in it's final destination qtFlattenRetCode = subprocess.call("/usr/libexec/podcastproducer/qtflatten '%s' '%s'" % (tempReferenceFilePath,self.outFilePath),shell=True,universal_newlines=True) if qtFlattenRetCode is not 0: self.logger("specialFunction('autoKey') Could not flatten movie:'%s' into movie:'%s' return code:'%s'" %(tempReferenceFilePath,self.outFilePath,qtFlattenRetCode), "error") return False ## cleanup support files ## os.rmtree(tempDir) return True
[docs] def pathForDevice(self, deviceName): """Returns a Path for an FCS device todo: this needs to be moved to master object""" path = False if deviceName == "Eshots Vid 1 FTP": path = os.path.join("/Volumes/FCServer HD/FTPUploads/E Shots/Video Crew 1") elif deviceName == "Eshots Video": path = os.path.join("/Volumes/FCServer HD/Media/Eshots") elif deviceName == "Eshots Video 2 FTP": path = os.path.join("/Volumes/FCServer HD/FTPUploads/E SHOTS/Video Crew 2") elif deviceName == "Library": path = os.path.join("/Volumes/FCServer HD/Library") elif deviceName == "Media": path = os.path.join("/Volumes/FCServer HD/Media") elif deviceName == "Support": path = os.path.join("/Volumes/FCServer HD/Support") elif deviceName == "Watchers": path = os.path.join("/Volumes/FCServer HD/Watchers") elif deviceName == "debugtest": path = os.path.join("/Users/hunterbj/Desktop/","Shake Project","samples") if not path: self.logger("pathForDevice() Path could not be found for device:'%s'" % deviceName) return False elif not os.path.exists(path): self.logger("pathForDevice() local path could not be found for device:'%s', resolved path:'%s'" % (deviceName,path)) return path