Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Osmiogrzesznik/7752fb7d934721462a64b6592f2ee7ef to your computer and use it in GitHub Desktop.
Save Osmiogrzesznik/7752fb7d934721462a64b6592f2ee7ef to your computer and use it in GitHub Desktop.
Very Big Batch Converter (why not to make Blender a video converter :) since it handles color space better out of the box, why even bother ? Try to shoot some videos with flat color profile and watch in dismay as most of smart software around by default crumbles it to dust , obliterating dark details. Had to do this having 148 videos and after 5…
'''Very Big Batch Converter (why not to make Blender a video converter :) since it handles color space better out of the box,
OK, but why even bother ? Try to shoot some videos with flat color profile and watch in dismay as most of smart software
around by default crumbles it to dust , obliterating dark details. Had to do this having 148 videos and after 5hours
of testing every single possible codec and a system renderer setting combination in VLC, ffmpeg etc. '''
# author:
# Conglomerate Mind Finding its temporary manifestation in form of SourceCowD,
# meaning i don't remember all my furious copy-pastes and countless google searches,
# all what i did was creating a synergical sum of separate , already existing components,
# all what we did is metabolise the information and spew it out
# WE ARE NOT FULLY FORMED YET
# FOR I AM MANY ©
# FOR WE ARE NANNY ©
#
# follow @sourcecowdmoomoo or @sourcecowd for more
import bpy
import os
from bpy.props import StringProperty, BoolProperty
from bpy_extras.io_utils import ImportHelper
from bpy.types import Operator
TIMER_INTERVAL = 10
renderpathorig = bpy.context.scene.render.filepath
isdir = os.path.isdir(renderpathorig)
glob_renderpath = renderpathorig
if not isdir:
glob_renderpath = os.path.dirname(renderpathorig)+os.sep
namepattern = renderpathorig.replace(glob_renderpath, "")
bothfix = namepattern.split("___")
if len(bothfix) < 2:
bothfix.append('')
prefix = bothfix[0]
postfix = bothfix[1]
def getListOfMovieFileTuplesSortedBySize(folderpath, extension=".MOV"):
path = folderpath
# gether all filenames in the directory
movie_files = [f for f in os.listdir(path) if f.endswith(extension)]
pathstomovies = [os.path.join(folderpath, f) for f in movie_files]
moviesizes = [os.stat(p).st_size for p in pathstomovies]
# sort movies by size (in case of any memory related
# crash it will at least do some work)
mf_ms = [(s, n) for s, n in zip(moviesizes, movie_files)]
mf_ms.sort(key=lambda a: a[0])
# give each tuple some id for easier debugging etc
mf_ms = [(s, n, i) for i, (s, n) in enumerate(mf_ms)]
return mf_ms
class OT_Video_BatchBlenderConvert(Operator, ImportHelper):
'''
VBBC - select a folder to batch render all videos with specified extension after adding to video sequencer(channel 1)\n Uses render output as a destination path and naming patter where ___(three underscores) is your old movie file name
'''
bl_idname = "video.batch_blender_conversion"
# TODO this is button description only
bl_label = "batchConvertRender"
_timer = None
cancelledDuringRender = False
listOfMovieFileTuples = None
stop = None
renderingAnimation = None
preparedForNextRender = None
path = glob_renderpath
baseFolderPath_inputFiles = None
currentStrip = None
currentMovieFileTuple = None
modalcall = 0
extension_str: StringProperty(
name="extension (case sensitive)",
description='use this extension to filter your movies',
maxlen=8,
default=".MOV"
)
removeOld: BoolProperty(
name="remove old",
description="remove original files after conversion",
default=False
)
removeOldConf: BoolProperty(
name="remove old2",
description="This is second Boolean just not to make accidental booboo remove original files after conversion",
default=False
)
def runThisAfterLoadingStripButBeforeNextRender(self):
pass
def say(self, msg, lv={'INFO'}):
print(msg)
self.report({'INFO'}, msg)
def reportDebug(self, msgpref, lv={'INFO'}):
if self.currentMovieFileTuple is None:
self.say(
msgpref + ' NO MOVIE LOADED YET')
return
self.say(
msgpref + f'{self.currentMovieFileTuple[2]}.{self.currentMovieFileTuple[1]} frames:{self.currentStrip.frame_final_end}, remaining:{len(self.listOfMovieFileTuples)}')
def VBBC_onRenderInit(self, scene, context=None):
self.reportDebug("INITIALISED RENDER:")
self.renderingAnimation = True
def VBBC_onRenderCancelled(self, scene, context=None):
self.reportDebug('CANCELLED during render:', lv={'WARNING'})
self.cancelledDuringRender = True
self.stop = True
self.renderingAnimation = False
def VBBC_onRenderComplete(self, scene, context=None):
print("render complete")
self.reportDebug('COMPLETED:')
self.renderingAnimation = False
self.preparedForNextRender = False
def execute(self, context):
print("executing!")
if not self.filepath:
return {'CANCELLED'}
self.baseFolderPath_inputFiles = self.filepath
isdir = os.path.isdir(self.filepath)
if not isdir:
self.baseFolderPath_inputFiles = os.path.dirname(
self.filepath)+os.sep
print("is directory:", isdir)
print('dirname:', self.baseFolderPath_inputFiles)
filename, extensionselectedfile = os.path.splitext(self.filepath)
print('Selected file:', self.filepath)
print('File name:', filename)
print('File extension:', extensionselectedfile)
self.stop = False
self.preparedForNextRender = False
self.renderingAnimation = False
listOfMovieFileTuplesSortedBySize = getListOfMovieFileTuplesSortedBySize(
self.baseFolderPath_inputFiles, extension=self.extension_str)
self.say(
f'there is {len(listOfMovieFileTuplesSortedBySize)} of {self.extension_str} files in {self.baseFolderPath_inputFiles} dir')
# return {'CANCELLED'}
# TODO create Lists in text files ?
# blenderCipher = open(os.path.join(
# self.baseFolderPath_inputFiles, "moviefilestoConvert.txt"), 'w')
# blenderCipher.write(
# f'Amount of files:{len(listOfMovieFileTuplesSortedBySize)}\n')
# for i, x in enumerate(listOfMovieFileTuplesSortedBySize):
# blenderCipher.write(f'{i},{x[1]}\n')
# blenderCipher.close()
# smallTestList = listOfMovieFileTuplesSortedBySize[0:3]
# self.listOfMovieFileTuples = smallTestList
self.listOfMovieFileTuples = listOfMovieFileTuplesSortedBySize
self.regAllHandlers(context)
return {"RUNNING_MODAL"}
def removeCurrentStripAndItsFile(self, removeConverted=True):
if self.currentStrip is not None:
oldfile = self.currentStrip.filepath
bpy.context.scene.sequence_editor.sequences.remove(
self.currentStrip)
if self.removeOld and self.removeOldConf and not self.cancelledDuringRender:
msg = f'strip removed,removing converted file from system: {oldfile}: next Movie: {self.currentMovieFileTuple[2]}.{self.currentMovieFileTuple[1]}, remaining:{len(self.listOfMovieFileTuples)}'
if removeConverted:
self.say(msg, lv={'WARNING'})
os.remove(oldfile)
else:
self.reportDebug("NOT " + msg)
self.currentStrip = None
def myCleanUp(self, context, removeConverted=True):
self.say("cleaning up")
self.removeCurrentStripAndItsFile(removeConverted)
bpy.app.handlers.render_init.remove(self.VBBC_onRenderInit)
bpy.app.handlers.render_complete.remove(self.VBBC_onRenderComplete)
bpy.app.handlers.render_cancel.remove(self.VBBC_onRenderCancelled)
context.window_manager.event_timer_remove(self._timer)
context.scene.render.filepath = renderpathorig
def regAllHandlers(self, context):
self.say("registering handlers")
bpy.app.handlers.render_complete.append(self.VBBC_onRenderComplete)
bpy.app.handlers.render_init.append(self.VBBC_onRenderInit)
bpy.app.handlers.render_cancel.append(self.VBBC_onRenderCancelled)
self._timer = context.window_manager.event_timer_add(
TIMER_INTERVAL, window=context.window)
context.window_manager.modal_handler_add(self)
def modal(self, context, event):
self.modalcall += 1
print("MODALCALL:", self.modalcall)
print("EVENT>TYPE:", event.type)
print("self.renderingAnimation:", self.renderingAnimation)
if event.type in {'ESC'}:
self.reportDebug("CANCELLED BY PRESSING BUTTON ESC:")
self.stop = True
self.myCleanUp(context, removeConverted=False)
return {'CANCELLED'}
if event.type == 'TIMER':
finishedAll = len(self.listOfMovieFileTuples) < 1
if (True in (finishedAll, self.stop is True)) and (not self.renderingAnimation):
self.reportDebug(f"FINISHED:")
self.myCleanUp(context, removeConverted=False)
print("Finished!")
return {'CANCELLED'}
elif self.renderingAnimation is False:
if self.preparedForNextRender is False:
try:
self.prepForNextAnimationRender()
self.reportDebug("PREPARED:")
bpy.ops.render.render(
"INVOKE_DEFAULT", animation=True, write_still=True)
self.preparedForNextRender = True
self.renderingAnimation = True
except:
self.myCleanUp(context, removeConverted=False)
raise
return {"PASS_THROUGH"}
def prepForNextAnimationRender(self):
'''this one only gets next movie item from collected list'''
filesize_filename_tuple = self.listOfMovieFileTuples.pop(0)
self.currentMovieFileTuple = filesize_filename_tuple
self.say(
f"next removed moviefile tuple self.currentMovieFileTuple:{self.currentMovieFileTuple[2]}.{self.currentMovieFileTuple[1]},remaining:{len(self.listOfMovieFileTuples)}")
fsize = filesize_filename_tuple[0]
filename = filesize_filename_tuple[1]
filepath = os.path.join(self.baseFolderPath_inputFiles, filename)
self.prepareStripsAndSceneForNextRender(
fsize, filepath, filename)
def prepareStripsAndSceneForNextRender(self, fsize, filepath, filename):
'''prepares sequencer and scene, adds strip based on movie file'''
self.removeCurrentStripAndItsFile()
print("Reading file: " + filepath)
self.currentStrip = bpy.context.scene.sequence_editor.sequences.new_movie(
name=filename, filepath=filepath, channel=1, frame_start=1, fit_method='ORIGINAL')
filename, extension = os.path.splitext(filename)
path_to_render_currentstrip_to = os.path.join(
glob_renderpath, prefix+"_"+filename+"_"+postfix)
print("filenamenoext:", filename)
print("renderpathorig:", renderpathorig)
print("glob_renderpath:", glob_renderpath)
print("prefix:", prefix)
print("postfix:", postfix)
print("path_to_render_currentstrip_to:",
path_to_render_currentstrip_to)
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = self.currentStrip.frame_final_end
bpy.context.scene.render.filepath = path_to_render_currentstrip_to
self.preparedForNextRender = True
self.runThisAfterLoadingStripButBeforeNextRender()
return
def register():
bpy.utils.register_class(OT_Video_BatchBlenderConvert)
def unregister():
bpy.utils.unregister_class(OT_Video_BatchBlenderConvert)
if __name__ == "__main__":
try:
unregister()
except Exception as err:
print("CATCHED UNREGISTER:", repr(err))
try:
for handlerList in (bpy.app.handlers.render_complete, bpy.app.handlers.render_init, bpy.app.handlers.render_complete):
for handler in handlerList:
print("CHECKING HANDLER:", handler.__name__)
if handler.__name__ in ('VBBC_onRenderInit', 'VBBC_onRenderCancelled', 'VBBC_onRenderComplete'):
print("REMOVING OLD HANDLER:", handler.__name__)
handlerList.remove(handler)
except Exception as err:
print("CATCHED HANDLER:", repr(err))
register()
# test call
bpy.ops.video.batch_blender_conversion('INVOKE_DEFAULT')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment