#!/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