from os.path import expandvars, abspath from pathlib import Path import bpy from bpy.types import Operator from bpy.props import (BoolProperty, StringProperty, FloatProperty, IntProperty, EnumProperty) from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels, get_channel_index, new_text_strip, get_strip_at, get_channel_name, create_shot_strip) from vse_toolbox import auto_splitter from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings from shutil import copy2 class VSETB_OT_rename(Operator): bl_idname = "vse_toolbox.strips_rename" bl_label = "Rename Strips" bl_description = "Rename Strips" bl_options = {"REGISTER", "UNDO"} #template : StringProperty(name="Strip Name", default="") #increment : IntProperty(name="Increment", default=0) selected_only : BoolProperty(name="Selected Only", default=True) #start_number : IntProperty(name="Start Number", default=0, min=0) #by_sequence : BoolProperty( # name="Reset By Sequence", # description="Reset Start Number for each sequence", # default=False #) @classmethod def poll(cls, context): settings = get_scene_settings() strip = context.active_sequence_strip return settings.active_project and get_channel_name(strip) in ('Shots', 'Sequences') def invoke(self, context, event): scn = context.scene settings = get_scene_settings() return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout scn = context.scene settings = get_scene_settings() project = settings.active_project episode = project.episode_name sequence = str(project.sequence_start_number).zfill(project.sequence_padding) shot = str(project.shot_start_number).zfill(project.shot_padding) strip = context.active_sequence_strip channel_name = get_channel_name(strip) col = layout.column() col.use_property_split = True col.use_property_decorate = False if channel_name == 'Shots': col.prop(project, 'shot_template', text='Shot Name') col.prop(project, 'shot_start_number', text='Start Number') col.prop(project, 'shot_increment', text='Increment') col.prop(project, 'shot_padding', text='Padding') col.prop(project, 'reset_by_sequence') elif channel_name == 'Sequences': col.prop(project, 'sequence_template' ,text='Sequence Name') col.prop(project, 'sequence_start_number', text='Start Number') col.prop(project, 'sequence_increment', text='Increment') col.prop(project, 'sequence_padding', text='Padding') col.prop(self, 'selected_only') if channel_name == 'Shots': label = project.shot_template.format(episode=episode, sequence=sequence, shot=shot) elif channel_name == 'Sequences': label = project.sequence_template.format(episode=episode, sequence=sequence) col.label(text=f'Renaming {label}') def execute(self, context): scn = context.scene settings = get_scene_settings() project = settings.active_project strip = context.active_sequence_strip channel_name = get_channel_name(strip) strips = get_strips(channel=channel_name, selected_only=self.selected_only) if channel_name == 'Shots': rename_strips(strips, template=project.shot_template, increment=project.shot_increment, start_number=project.shot_start_number, by_sequence=project.reset_by_sequence, padding=project.shot_padding ) if channel_name == 'Sequences': rename_strips(strips, template=project.sequence_template, increment=project.sequence_increment, start_number=project.sequence_start_number, padding=project.sequence_padding ) return {"FINISHED"} class VSETB_OT_show_waveform(Operator): bl_idname = "vse_toolbox.show_waveform" bl_label = "Show Waveform" bl_description = "Show Waveform of all audio strips" bl_options = {"REGISTER", "UNDO"} enabled : BoolProperty(default=True) @classmethod def poll(cls, context): return True def execute(self, context): scn = context.scene for strip in get_strips(channel='Audio'): strip.show_waveform = self.enabled return {"FINISHED"} class VSETB_OT_set_sequencer(Operator): bl_idname = "vse_toolbox.set_sequencer" bl_label = "Set Sequencer" bl_description = "Set resolution, frame end and channel names" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return True def execute(self, context): scn = context.scene set_channels() movies = get_strips(channel='Movie') movie = None if movies: movie = movies[0] movie.transform.scale_x = movie.transform.scale_y = 1 elem = movie.strip_elem_from_frame(scn.frame_current) scn.render.resolution_x = elem.orig_width scn.render.resolution_y = elem.orig_height else: self.report({'INFO'}, f'Cannot set Resolution. No Movie Found.') scn.view_settings.view_transform = 'Standard' scn.render.image_settings.file_format = 'FFMPEG' scn.render.ffmpeg.gopsize = 8 scn.render.ffmpeg.constant_rate_factor = 'HIGH' scn.render.ffmpeg.format = 'QUICKTIME' scn.render.ffmpeg.audio_codec = 'AAC' scn.render.ffmpeg.audio_codec = 'MP3' scn.render.ffmpeg.audio_mixrate = 44100 scn.render.ffmpeg.audio_bitrate = 128 shots = get_strips(channel='Shots') if shots: scn.frame_end = shots[-1].frame_final_end -1 elif movie: scn.frame_end = movie.frame_final_end -1 return {"FINISHED"} class VSETB_OT_auto_split(Operator): """Launch subprocess with ffmpeg and python to find and create each shots strips from video source""" bl_idname = "vse_toolbox.auto_split" bl_label = "Auto Split" bl_description = "Generate shots strips" bl_options = {"REGISTER", "UNDO"} threshold: FloatProperty(name="Threshold", default=0.6, min=0, max=1) frame_first: IntProperty(name='Start Split') frame_last: IntProperty(name='End Split') movie_channel_name: EnumProperty( items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels), name='Movie Channel') def invoke(self, context, event): self.frame_first = context.scene.frame_start self.frame_last = context.scene.frame_end if context.selected_sequences: self.frame_first = min([s.frame_final_start for s in context.selected_sequences]) self.frame_last = max([s.frame_final_end for s in context.selected_sequences]) return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout col = layout.column() col.use_property_split = True col.use_property_decorate = False col.prop(self, 'threshold') col.prop(self, 'movie_channel_name') split_col = col.column(align=True) split_col.prop(self, 'frame_first', text='Frame Split First') split_col.prop(self, 'frame_last', text='Last') def execute(self, context): context.window_manager.modal_handler_add(self) return {'PASS_THROUGH'} def modal(self, context, event): strips = get_strips(channel=self.movie_channel_name) i = 1 frame_start = self.frame_first for strip in strips: if strip.type != 'MOVIE': continue # Skip strip outside the frame range to create shot from. if strip.frame_final_start >= self.frame_last or strip.frame_final_end <= self.frame_first: continue process = auto_splitter.launch_split(strip, self.threshold, frame_start=self.frame_first, frame_end=self.frame_last) for line in process.stdout: # Get frame split from the movie timeline (not from blender strips timeline) frame_end = auto_splitter.get_split_time(line, fps=24) if not frame_end: continue # Convert movie frame to strips frame if frame_start+int(strip.frame_start) < self.frame_first: frame_start = self.frame_first frame_end += int(strip.frame_final_start) if frame_end > self.frame_last: frame_end = self.frame_last create_shot_strip( f'tmp_shot_{str(i).zfill(3)}', start=frame_start, end=frame_end ) i += 1 frame_start = frame_end bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) process.wait() # Last strip: if frame_start < self.frame_last: create_shot_strip( f'tmp_shot_{str(i).zfill(3)}', start=frame_start, end=self.frame_last ) return {'FINISHED'} class VSETB_OT_set_stamps(Operator): bl_idname = "vse_toolbox.set_stamps" bl_label = "Set Stamps" bl_description = "Set Stamps on Video" bl_options = {"REGISTER", "UNDO"} def execute(self, context): scn = context.scene settings = get_scene_settings() project = settings.active_project #strip_settings = get_strip_settings() channel_index = get_channel_index('Stamps') for strip in get_strips('Stamps'): if strip.type == 'META': scn.sequence_editor.sequences.remove(strip) bpy.ops.sequencer.select_all(action='DESELECT') height = scn.render.resolution_y width = scn.render.resolution_x ratio = (height / 1080) margin = 0.01 box_margin = 0.005 font_size = int(24*ratio) crop_x = int(width * 0.4) crop_max_y = int(height - font_size*2) #crop_min_y = int(scn.render.resolution_y * 0.01) stamp_params = dict(start=scn.frame_start, end=scn.frame_end, font_size=font_size, y=margin, box_margin=box_margin, select=True, box_color=(0, 0, 0, 0.5)) # Project Name project_text = '{project}' if project.type == 'TVSHOW': project_text = '{project} / ep{episode}' project_strip_stamp = new_text_strip('project_stamp', channel=1, **stamp_params, text=project_text, x=0.01, align_x='LEFT', align_y='BOTTOM') project_strip_stamp.crop.max_x = crop_x * 2 project_strip_stamp.crop.max_y = crop_max_y # Shot Name shot_strip_stamp = new_text_strip('shot_stamp', channel=2, **stamp_params, text='sq{sequence} / sh{shot}', align_y='BOTTOM') shot_strip_stamp.crop.min_x = crop_x shot_strip_stamp.crop.max_x = crop_x shot_strip_stamp.crop.max_y = crop_max_y # Frame frame_strip_stamp = new_text_strip('frame_stamp', channel=3, **stamp_params, text='{shot_frame} / {shot_duration} {timecode}', x=0.99, align_x='RIGHT', align_y='BOTTOM') frame_strip_stamp.crop.min_x = crop_x *2 frame_strip_stamp.crop.max_y = crop_max_y bpy.ops.sequencer.meta_make() stamps_strip = context.active_sequence_strip stamps_strip.name = 'Stamps' stamps_strip.channel = channel_index #stamps_strip = scn.sequence_editor.sequences.new_meta('Stamps', scn.frame_start, scn.frame_end) #stamps_strip.channel = get_channel_index('Stamps') scn.frame_set(scn.frame_current) # For update stamps return {"FINISHED"} class VSETB_OT_previous_shot(Operator): bl_idname = "vse_toolbox.previous_shot" bl_label = "Jump to Previous Shot" bl_description = "Jump to Previous Shot" bl_options = {"REGISTER", "UNDO"} def execute(self, context): strips = get_strips('Shots') if not strips: return {"CANCELLED"} active_strip = get_strip_at('Shots') if active_strip is strips[0]: return {"CANCELLED"} active_strip_index = strips.index(active_strip) next_shot = strips[active_strip_index - 1] context.scene.frame_set(next_shot.frame_final_start) bpy.ops.sequencer.select_all(action="DESELECT") next_shot.select = True context.scene.sequence_editor.active_strip = next_shot return {"FINISHED"} class VSETB_OT_next_shot(Operator): bl_idname = "vse_toolbox.next_shot" bl_label = "Jump to Next Shot" bl_description = "Jump to Next Shot" bl_options = {"REGISTER", "UNDO"} def execute(self, context): strips = get_strips('Shots') if not strips: return {"CANCELLED"} active_strip = get_strip_at('Shots') if active_strip is strips[-1]: return {"CANCELLED"} active_strip_index = strips.index(active_strip) next_shot = strips[active_strip_index + 1] context.scene.frame_set(next_shot.frame_final_start) bpy.ops.sequencer.select_all(action="DESELECT") next_shot.select = True context.scene.sequence_editor.active_strip = next_shot return {"FINISHED"} class VSETB_OT_open_strip_folder(Operator): bl_idname = "vse_toolbox.open_strip_folder" bl_label = "Open Strip Folder" bl_description = "Open selected strip folder" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): strip = context.active_sequence_strip if not strip: cls.poll_message_set('No active') return if not any(p in get_channel_name(strip) for p in ('Movie', 'Video', 'Audio', 'Sound')): cls.poll_message_set('No active Movie or Audio strip') return return True def execute(self, context): tpl_by_channel = { 'Shots': 'shot_dir', 'Sequences': 'sequence_dir' } strip = context.active_sequence_strip settings = get_scene_settings() project = settings.active_project strip_settings = get_strip_settings() format_data = {**settings.format_data, **project.format_data, **strip_settings.format_data} channel_name = get_channel_name(strip) if strip.type in ('MOVIE', 'IMAGE'): path = Path(strip.filepath) elif strip.type in ('SOUND'): path = Path(strip.sound.filepath) else: folder_template = expandvars(project.templates[tpl_by_channel[channel_name]].value) path = Path(folder_template.format(**format_data)) if not path.is_dir(): path = path.parent bpy.ops.wm.path_open(filepath=str(path)) return {"FINISHED"} class VSETB_OT_collect_files(Operator): bl_idname = "vse_toolbox.collect_files" bl_label = "Collect Files" bl_description = "Collect Files" bl_options = {"REGISTER", "UNDO"} collect_folder: StringProperty( name="Collect Folder", default="//sources", subtype='DIR_PATH') @classmethod def poll(cls, context): if bpy.data.is_saved: return True cls.poll_message_set('Save the blend to collect files') def execute(self, context): strip = context.active_sequence_strip settings = get_scene_settings() project = settings.active_project strips = [s for s in context.scene.sequence_editor.sequences_all if s.type in ('MOVIE', 'SOUND')] context.window_manager.progress_begin(0, len(strips)) for i, strip in enumerate(strips): context.window_manager.progress_update(i) if strip.type == 'MOVIE': src_path = strip.filepath elif strip.type == 'SOUND': src_path = strip.sound.filepath src_path = Path(abspath(bpy.path.abspath(src_path))) dst_path = Path(bpy.path.abspath(self.collect_folder), src_path.name) if src_path == dst_path: continue dst_path.parent.mkdir(exist_ok=True, parents=True) print(f'Copy file from {src_path} to {dst_path}') copy2(str(src_path), str(dst_path)) rel_path = bpy.path.relpath(str(dst_path)) if len(Path(rel_path).as_posix()) < len(dst_path.as_posix()): dst_path = rel_path if strip.type == 'MOVIE': strip.filepath = str(dst_path) elif strip.type == 'SOUND': strip.sound.filepath = str(dst_path) context.window_manager.progress_end() return {"FINISHED"} def invoke(self, context, event): scn = context.scene settings = get_scene_settings() return context.window_manager.invoke_props_dialog(self) class VSETB_OT_insert_channel(Operator): bl_idname = "vse_toolbox.insert_channel" bl_label = "Insert Channel" bl_description = "Insert a Channel bellow the active strip" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.active_sequence_strip def execute(self, context): scn = context.scene channel_index = context.active_sequence_strip.channel strips = list(scn.sequence_editor.sequences) for strip in sorted(strips, key=lambda x: x.channel, reverse=True): if strip.channel >= channel_index: strip.channel += 1 channels = {i: (c.name, c.lock, c.mute) for i, c in enumerate(scn.sequence_editor.channels)} for i in sorted(channels.keys()): channel = scn.sequence_editor.channels[i] #i = list(scn.sequence_editor.channels).index(i) prev_channel = channels.get(i-1) if i == channel_index: channel.name = "Channel" channel.lock = False channel.mute = False elif i >= channel_index and prev_channel is not None: prev_name, prev_lock, prev_mute = prev_channel channel.name = prev_name if channel.lock != prev_lock: channel.lock = prev_lock if channel.mute != prev_mute: channel.mute = prev_mute return {'FINISHED'} class VSETB_OT_remove_channel(Operator): bl_idname = "vse_toolbox.remove_channel" bl_label = "Remove Channel" bl_description = "Remove Channel bellow the active strip" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.active_sequence_strip def execute(self, context): scn = context.scene channel_index = context.active_sequence_strip.channel if [s for s in scn.sequence_editor.sequences if s.channel == channel_index-1]: self.report({"WARNING"}, "Channel Bellow not empty") strips = list(scn.sequence_editor.sequences) for strip in sorted(strips, key=lambda x: x.channel): if strip.channel >= channel_index: strip.channel -= 1 channels = {i: (c.name, c.lock, c.mute) for i, c in enumerate(scn.sequence_editor.channels)} for i in sorted(channels.keys(), reverse=True): channel = scn.sequence_editor.channels[i] #i = list(scn.sequence_editor.channels).index(i) prev_channel = channels.get(i+1) if i >= channel_index-1 and prev_channel is not None: prev_name, prev_lock, prev_mute = prev_channel channel.name = prev_name if channel.lock != prev_lock: channel.lock = prev_lock if channel.mute != prev_mute: channel.mute = prev_mute return {'FINISHED'} class WM_OT_split_view(Operator): """Toggle Split sequencer view""" bl_idname = "wm.split_view" bl_label = "Split View" def get_preview_areas(self, context): return [ x for x in context.screen.areas if x.type == "SEQUENCE_EDITOR" and x.spaces[0].view_type == "PREVIEW" ] def invoke(self, context, event): preview_areas = self.get_preview_areas(context) if not preview_areas: return {"CANCELLED"} scn = context.scene video_channels = [i for i, c in enumerate(scn.sequence_editor.channels) if 'Video' in c.name] if len(video_channels) < 2: self.report({"ERROR"}, 'You need two channels to split the view') return {"CANCELLED"} if len(preview_areas) == 1: # Split area with bpy.context.temp_override(area=preview_areas[0]): bpy.ops.screen.area_split(direction="VERTICAL") # Disable toolbar on right panel # Update areas preview_areas = self.get_preview_areas(context) preview_areas[-1].spaces[0].display_channel = 0 #video_channels[-1] preview_areas[0].spaces[0].display_channel = video_channels[-2] preview_areas[0].spaces[0].show_gizmo_navigate = False preview_areas[-1].spaces[0].show_gizmo_navigate = False # Hide toolbar #preview_areas[0].spaces[0].show_region_toolbar = False else: # Give the remaining area to have the left area's channel displayed # Show toolbar of remaning area #preview_areas[0].spaces[0].show_region_toolbar = True # Join areas # Simulates the mouse position as being between the two areas horizontally # and a bit above the bottom corner vertically cursor_x = int(preview_areas[0].x - (preview_areas[0].x - preview_areas[1].width) / 2) cursor_y = preview_areas[0].y + 10 bpy.ops.screen.area_join(cursor=(cursor_x, cursor_y)) preview_areas = self.get_preview_areas(context) preview_areas[0].spaces[0].display_channel = 0 # Force UI update, due to Blender bug, delete when fixed -> https://developer.blender.org/T65529 bpy.ops.screen.area_swap(cursor=(preview_areas[0].x, preview_areas[0].y)) bpy.ops.screen.area_swap(cursor=(preview_areas[0].x, preview_areas[0].y)) return {"FINISHED"} class VSETB_OT_merge_shot_strips(Operator): """Merge selected shots strips (required at least two strips).""" bl_idname = "vse_toolbox.merge_shot_strips" bl_label = "Merge Shot Strips" @classmethod def poll(cls, context): selected_strips = bpy.context.selected_sequences if len(selected_strips) <= 1: return False return all(get_channel_name(strip) == 'Shots' for strip in selected_strips) def execute(self, context): selected_strips = bpy.context.selected_sequences last_frame = selected_strips[-1].frame_final_end for i in range(1, len(selected_strips)): context.scene.sequence_editor.sequences.remove(selected_strips[i]) selected_strips[0].frame_final_end = last_frame return {"FINISHED"} addon_keymaps = [] def register_keymaps(): addon = bpy.context.window_manager.keyconfigs.addon if not addon: return #print('VSE Toolbox Keymaps Register') km = addon.keymaps.new(name="Sequencer", space_type="SEQUENCE_EDITOR") kmi = km.keymap_items.new('vse_toolbox.previous_shot', type='LEFT_ARROW', value='PRESS', ctrl=True) addon_keymaps.append((km, kmi)) kmi = km.keymap_items.new('vse_toolbox.next_shot', type='RIGHT_ARROW', value='PRESS', ctrl=True) addon_keymaps.append((km, kmi)) kmi = km.keymap_items.new('vse_toolbox.merge_shot_strips', type='J', value='PRESS', ctrl=True) addon_keymaps.append((km, kmi)) def unregister_keymaps(): #print('unregister_keymaps', addon_keymaps) for km, kmi in addon_keymaps: if kmi in list(km.keymap_items): km.keymap_items.remove(kmi) addon_keymaps.clear() classes = ( VSETB_OT_rename, VSETB_OT_set_sequencer, VSETB_OT_auto_split, VSETB_OT_set_stamps, VSETB_OT_show_waveform, VSETB_OT_previous_shot, VSETB_OT_next_shot, VSETB_OT_open_strip_folder, VSETB_OT_collect_files, VSETB_OT_insert_channel, VSETB_OT_remove_channel, VSETB_OT_merge_shot_strips, WM_OT_split_view, ) def register(): for cls in classes: bpy.utils.register_class(cls) register_keymaps() def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) unregister_keymaps()