Last active
March 4, 2023 17:21
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'''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