diff --git a/.gitignore b/.gitignore index 98c602f..81bdb39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *pyc -__pycache__ \ No newline at end of file +__pycache__ +.vscode \ No newline at end of file diff --git a/__init__.py b/__init__.py index 1565c90..f95db75 100644 --- a/__init__.py +++ b/__init__.py @@ -17,25 +17,21 @@ import sys from pathlib import Path sys.modules['vse_toolbox'] = sys.modules.pop(Path(__file__).parent.name) -from vse_toolbox import panels -from vse_toolbox import preferences -from vse_toolbox import properties +from vse_toolbox import ui from vse_toolbox import operators from vse_toolbox.constants import ASSET_PREVIEWS from vse_toolbox.sequencer_utils import set_active_strip, update_text_strips modules = ( - properties, - preferences, operators, - panels, + ui, ) if 'bpy' in locals(): import importlib - for module in modules: - importlib.reload(module) + for mod in modules: + importlib.reload(mod) import bpy @@ -49,8 +45,8 @@ def register(): bpy.app.handlers.frame_change_post.append(set_active_strip) bpy.app.handlers.frame_change_post.append(update_text_strips) -def unregister(): +def unregister(): try: bpy.utils.previews.remove(ASSET_PREVIEWS) except Exception as e: diff --git a/operators/__init__.py b/operators/__init__.py index becfa5c..447cc04 100644 --- a/operators/__init__.py +++ b/operators/__init__.py @@ -1,16 +1,30 @@ # SPDX-License-Identifier: GPL-2.0-or-later -from vse_toolbox.operators import operators +from vse_toolbox.operators import (addon, casting, imports, render, sequencer, + spreadsheet, tracker) + +modules = ( + addon, + casting, + imports, + render, + sequencer, + spreadsheet, + tracker +) if 'bpy' in locals(): import importlib - importlib.reload(operators) + for mod in modules: + importlib.reload(mod) import bpy def register(): - operators.register() + for mod in modules: + mod.register() def unregister(): - operators.unregister() \ No newline at end of file + for mod in modules: + mod.unregister() \ No newline at end of file diff --git a/operators/addon.py b/operators/addon.py new file mode 100644 index 0000000..3f17ff6 --- /dev/null +++ b/operators/addon.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +import importlib +import os + +import bpy +from bpy.types import Operator + +import vse_toolbox + + +from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings) +from vse_toolbox.file_utils import (read_file, ) + + + +class VSETB_OT_reload_addon(Operator): + bl_idname = "vse_toolbox.reload_addon" + bl_options = {"UNDO"} + bl_label = 'Reload VSE ToolBox Addon' + bl_description = 'Reload The VSE ToolBox Addon' + + def execute(self, context): + + print('Execute reload') + + vse_toolbox.unregister() + importlib.reload(vse_toolbox) + vse_toolbox.register() + + return {'FINISHED'} + + +class VSETB_OT_load_settings(Operator): + bl_idname = "vse_toolbox.load_settings" + bl_options = {"UNDO"} + bl_label = 'Load Settings' + bl_description = 'Load VSE ToolBox settings from config file' + + def execute(self, context): + prefs = get_addon_prefs() + settings = get_scene_settings() + project = settings.active_project + + if not prefs.config_path: + return + + addon_config = read_file(os.path.expandvars(prefs.config_path)) + + addon_config['trackers'] = addon_config.get('trackers') + trackers = addon_config.pop('trackers') + + addon_config['spreadsheet'] = addon_config.get('spreadsheet') + spreadsheet = addon_config.pop('spreadsheet') + + project_name = addon_config.get('project_name') + if project_name: + settings.project_name = project_name + + # Project Properties + for k, v in addon_config.items(): + try: + setattr(project, k, v) + except Exception: + print(f'Could not set property {k} with value {v} to project {settings.project_name}') + + if spreadsheet.get('cells'): + #project.spreadsheet.clear() + + for i, cell_data in enumerate(spreadsheet['cells']): + if not 'name' in cell_data: + print(f'cell_data {cell_data} need to have a attribute name') + continue + + cell = project.spreadsheet.get(cell_data['name']) + + if not cell: + print(f"cell {cell_data['name']} not in spreadsheet") + continue + + project.spreadsheet.move(list(project.spreadsheet).index(cell), i) + + for prop_name in ('export_name', 'enabled'): + if prop_name in cell_data: + setattr(cell, prop_name, cell_data[prop_name]) + + if spreadsheet.get('options'): + for k, v in spreadsheet['options'].items(): + try: + setattr(project.spreadsheet_options, k, v) + except Exception: + print(f'Could not set option {k} with value {v} to spreadsheet') + + return {'FINISHED'} + + +classes = ( + VSETB_OT_reload_addon, + VSETB_OT_load_settings +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/casting.py b/operators/casting.py new file mode 100644 index 0000000..13c463a --- /dev/null +++ b/operators/casting.py @@ -0,0 +1,327 @@ + +import json + +import bpy +from bpy.types import PropertyGroup, Operator +from bpy.props import (CollectionProperty, EnumProperty, StringProperty) + +from vse_toolbox.constants import CASTING_BUFFER +from vse_toolbox.sequencer_utils import get_strips +from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings, get_strip_settings) + + +class VSETB_OT_casting_replace(Operator): + bl_idname = "vse_toolbox.casting_replace" + bl_label = "Replace Asset" + bl_description = "Replace Asset of selected strips" + + old_asset : StringProperty() + new_asset : StringProperty() + + assets : CollectionProperty(type=PropertyGroup) + + def execute(self, context): + prefs = get_addon_prefs() + settings = get_scene_settings() + + new_asset = next(a for a in settings.active_project.assets if a.tracker_name == self.new_asset) + + for strip in get_strips('Shots', selected_only=True): + strip_settings = strip.vsetb_strip_settings + for asset_casting in strip_settings.casting: + if asset_casting.asset.tracker_name == self.old_asset: + + print(f'Replace casting on {strip.name}') + + asset_casting.name = new_asset.name + asset_casting.id = new_asset.id + asset_casting['_name'] = new_asset.label + + strip_settings.casting.update() + + self.assets.clear() + + return {'FINISHED'} + + def invoke(self, context, event): + settings = get_scene_settings() + project = settings.active_project + + self.assets.clear() + for asset in project.assets: + item = self.assets.add() + item.name = asset.tracker_name + + strip = context.active_sequence_strip + asset_casting_index = strip.vsetb_strip_settings.casting_index + active_asset = strip.vsetb_strip_settings.casting[asset_casting_index].asset + + self.old_asset = active_asset.tracker_name + self.new_asset = '' + + return context.window_manager.invoke_props_dialog(self) + # + + def draw(self, context): + scn = context.scene + settings = get_scene_settings() + + project = settings.active_project + + layout = self.layout + col = layout.column() + col.use_property_split = True + col.prop_search(self, 'old_asset', self, 'assets', text='Old Asset', icon='ASSET_MANAGER') + col.prop_search(self, 'new_asset', self, 'assets', text='New Asset', icon='ASSET_MANAGER') + + +def get_asset_items(self, context): + settings = get_scene_settings() + project = settings.active_project + + return [(a.id, a.label, '', i) for i, a in enumerate(project.assets)] + +class VSETB_OT_casting_add(Operator): + bl_idname = "vse_toolbox.casting_add" + bl_label = "Casting Add" + bl_description = "Add Asset to Castin" + bl_options = {"REGISTER", "UNDO"} + bl_property = "asset_name" + + asset_name : EnumProperty(name='', items=get_asset_items) + @classmethod + def poll(cls, context): + active_strip = context.scene.sequence_editor.active_strip + if active_strip: + return True + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {'FINISHED'} + + def execute(self, context): + scn = context.scene + active_strip = scn.sequence_editor.active_strip + + settings = get_scene_settings() + strip_settings = get_strip_settings() + + project = settings.active_project + + if strip_settings.casting.get(self.asset_name): + asset = project.assets[self.asset_name] + self.report({'WARNING'}, f"Asset {asset.label} already casted.") + return {"CANCELLED"} + + item = strip_settings.casting.add() + asset = project.assets[self.asset_name] + + item.name = asset.name + item.id = project.assets[self.asset_name].id + item['_name'] = asset.label + + strip_settings.casting.update() + + return {"FINISHED"} + + +class VSETB_OT_casting_remove(Operator): + bl_idname = "vse_toolbox.casting_remove" + bl_label = "Remove Item from Casting" + bl_description = "Remove Item from Casting" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + active_strip = context.scene.sequence_editor.active_strip + if active_strip: + return True + + def invoke(self, context, event): + scn = context.scene + + strip_settings = get_strip_settings() + idx = strip_settings.casting_index + + try: + item = strip_settings.casting[idx] + except IndexError: + pass + else: + item = strip_settings.casting[strip_settings.casting_index] + label = item.asset.label if item.asset.label else 'Empty' + info = f"Item {label} removed from casting" + strip_settings.casting.remove(idx) + + if strip_settings.casting_index == 0: + strip_settings.casting_index = 0 + else: + strip_settings.casting_index -= 1 + + self.report({'INFO'}, info) + + return {"FINISHED"} + + +class VSETB_OT_casting_move(Operator): + bl_idname = "vse_toolbox.casting_move" + bl_label = "Move Casting items" + bl_description = "Move Casting items" + bl_options = {"REGISTER", "UNDO"} + + direction: EnumProperty( + items=( + ('UP', "Up", ""), + ('DOWN', "Down", ""), + ) + ) + + asset_name : StringProperty() + + @classmethod + def poll(cls, context): + active_strip = context.scene.sequence_editor.active_strip + if active_strip: + return True + + def execute(self, context): + scn = context.scene + + strip_settings = get_strip_settings() + idx = strip_settings.casting_index + + try: + item = strip_settings.casting[idx] + except IndexError: + pass + else: + if self.direction == 'DOWN' and idx < len(strip_settings.casting) - 1: + item_next = strip_settings.casting[idx+1].name + strip_settings.casting.move(idx, idx+1) + strip_settings.casting_index += 1 + + elif self.direction == 'UP' and idx >= 1: + item_prev = strip_settings.casting[idx-1].name + strip_settings.casting.move(idx, idx-1) + strip_settings.casting_index -= 1 + + info = f"Item {item.asset.label} moved to position {(item.asset.label, strip_settings.casting_index + 1)}" + self.report({'INFO'}, info) + + return {"FINISHED"} + +class VSETB_OT_copy_casting(Operator): + bl_idname = "vse_toolbox.copy_casting" + bl_label = "Copy Casting" + bl_description = "Copy Casting from active strip" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + active_strip = context.scene.sequence_editor.active_strip + strip_settings = get_strip_settings() + + if active_strip and strip_settings.casting: + return True + + def execute(self, context): + scn = context.scene + + active_strip = scn.sequence_editor.active_strip + strip_settings = get_strip_settings() + + datas = [c.to_dict() for c in strip_settings.casting] + CASTING_BUFFER.write_text(json.dumps(datas), encoding='utf-8') + + return {"FINISHED"} + + +class VSETB_OT_paste_casting(Operator): + bl_idname = "vse_toolbox.paste_casting" + bl_label = "Paste Casting" + bl_description = "Paste Casting to active strip" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + active_strip = context.scene.sequence_editor.active_strip + if active_strip: + return True + + def execute(self, context): + scn = context.scene + strip_settings = get_strip_settings() + + if not CASTING_BUFFER.exists(): + self.report({'ERROR'}, f'No Casting to copy.') + return {"CANCELLED"} + + casting_datas = json.loads(CASTING_BUFFER.read_text()) + + + + for strip in context.selected_sequences: + strip_settings = strip.vsetb_strip_settings + for casting_data in casting_datas: + + item = strip.vsetb_strip_settings.casting.add() + + item.name = casting_data['name'] + item.id = casting_data['id'] + item['_name'] = casting_data['_name'] + + strip_settings.casting.update() + + return {"FINISHED"} + + +class VSETB_OT_copy_metadata(Operator): + bl_idname = "vse_toolbox.copy_metadata" + bl_label = "Copy metadata to selected" + bl_description = "Copy Metadata to selected strips" + + metadata : StringProperty() + + @classmethod + def poll(cls, context): + return context.selected_sequences and context.active_sequence_strip + + def execute(self, context): + prefs = get_addon_prefs() + settings = get_scene_settings() + project = settings.active_project + + metadata = next((m.field_name for m in project.metadata_types if m.name == self.metadata), None) + + if not metadata: + self.report({'ERROR'}, f'No Metadata named {self.metadata}') + + active_strip = context.active_sequence_strip + metadata_value = getattr(active_strip.vsetb_strip_settings.metadata, metadata) + + for strip in context.selected_sequences: + if strip == context.active_sequence_strip: + continue + + setattr(strip.vsetb_strip_settings.metadata, metadata, metadata_value) + + return {"FINISHED"} + + +classes = ( + VSETB_OT_casting_add, + VSETB_OT_casting_remove, + VSETB_OT_casting_move, + VSETB_OT_copy_casting, + VSETB_OT_paste_casting, + VSETB_OT_casting_replace, + VSETB_OT_copy_metadata +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/imports.py b/operators/imports.py new file mode 100644 index 0000000..ab0ad56 --- /dev/null +++ b/operators/imports.py @@ -0,0 +1,184 @@ + +from pathlib import Path + +import bpy +from bpy.types import Operator +from bpy.props import (CollectionProperty, BoolProperty, EnumProperty, StringProperty) + +from vse_toolbox.constants import (EDITS, EDIT_SUFFIXES, MOVIES, MOVIE_SUFFIXES, + SOUNDS, SOUND_SUFFIXES) + +from vse_toolbox.sequencer_utils import (clean_sequencer, import_edit, import_movie, import_sound) + +from vse_toolbox.bl_utils import get_scene_settings +from vse_toolbox.file_utils import install_module + + +class VSETB_OT_auto_select_files(Operator): + bl_idname = "import.auto_select_files" + bl_label = "Auto Select" + bl_description = "Auto Select Files" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return True + + def get_items(self, items=[]): + if not items: + return [('NONE', 'None', '', 0)] + return [(e, e, '', i) for i, e in enumerate(sorted(items))] + + def execute(self, context): + params = context.space_data.params + directory = Path(params.directory.decode()) + + EDITS.clear() + MOVIES.clear() + SOUNDS.clear() + + edits = [] + movies = [] + sounds = [] + + for file_entry in directory.glob('*'): + if file_entry.is_dir(): + continue + + if file_entry.suffix in EDIT_SUFFIXES: + edits.append(file_entry.name) + elif file_entry.suffix in MOVIE_SUFFIXES: + movies.append(file_entry.name) + elif file_entry.suffix in SOUND_SUFFIXES: + sounds.append(file_entry.name) + + EDITS.extend(self.get_items(items=edits)) + MOVIES.extend(self.get_items(items=movies)) + SOUNDS.extend(self.get_items(items=sounds)) + + return {'FINISHED'} + + +class VSETB_OT_import_files(Operator): + bl_idname = "vse_toolbox.import_files" + bl_label = "Import" + bl_description = "Import Edit" + bl_options = {"REGISTER", "UNDO"} + + directory : StringProperty(subtype='DIR_PATH') + filepath: StringProperty( + name="File Path", + description="Filepath used for importing the file", + maxlen=1024, + subtype='FILE_PATH', + ) + files : CollectionProperty(type=bpy.types.OperatorFileListElement) + clean_sequencer : BoolProperty( + name="Clean Sequencer", + default=False, + description="Clean all existing strips in sequencer", + ) + + import_edit : BoolProperty(name='', default=True) + edit: EnumProperty(name='', items=lambda s, c: EDITS) + + import_movie : BoolProperty(name='', default=False) + movie: EnumProperty(name='', items=lambda s, c: MOVIES) + + import_sound : BoolProperty(name='', default=False) + sound: EnumProperty(name='', items=lambda s, c: SOUNDS) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + scn = context.scene + settings = get_scene_settings() + + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + col = layout.column(align=True) + col.operator('import.auto_select_files', text='Auto Select') + + row = self.layout.row(heading="Import Edit", align=True) + row.prop(self, 'import_edit') + sub = row.row(align=True) + sub.active = self.import_edit + sub.prop(self, 'edit') + + row = self.layout.row(heading="Import Movie", align=True) + row.prop(self, 'import_movie') + sub = row.row() + sub.active = self.import_movie + sub.prop(self, 'movie') + + row = self.layout.row(heading="Import Sound", align=True) + row.prop(self, 'import_sound') + sub = row.row() + sub.active = self.import_sound + sub.prop(self, 'sound') + + col = layout.column() + col.separator() + col.prop(self, 'clean_sequencer') + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + def execute(self, context): + otio = install_module('opentimelineio') + + edit_filepath = Path(self.directory, self.edit) + if not edit_filepath.exists(): + self.import_edit = False + + movie_filepath = Path(self.directory, self.movie) + if not movie_filepath.exists(): + self.import_movie = False + + sound_filepath = Path(self.directory, self.sound) + if not sound_filepath.exists(): + self.import_sound = False + + if self.clean_sequencer: + clean_sequencer( + edit=self.import_edit, + movie=self.import_movie, + sound=self.import_sound, + ) + + if self.import_edit: + print(f'[>.] Loading Edit from: {str(edit_filepath)}') + import_edit(edit_filepath, adapter="cmx_3600") + + if self.import_movie: + print(f'[>.] Loading Movie from: {str(movie_filepath)}') + import_movie(movie_filepath) + + if self.import_sound: + print(f'[>.] Loading Audio from: {str(sound_filepath)}') + import_sound(sound_filepath) + elif not self.import_sound and self.import_movie: + print(f'[>.] Loading Audio from Movie: {str(movie_filepath)}') + import_sound(movie_filepath) + + context.scene.sequence_editor.sequences.update() + + return {"FINISHED"} + +classes = ( + VSETB_OT_auto_select_files, + VSETB_OT_import_files, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/operators.py b/operators/operators.py deleted file mode 100644 index bf512cf..0000000 --- a/operators/operators.py +++ /dev/null @@ -1,1424 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -import importlib -import json -import re -import time -from pprint import pprint -import csv -from datetime import datetime -import os -from pathlib import Path - -import bpy -from bpy.types import (Operator, Menu) -from bpy_extras.io_utils import ImportHelper -from bpy.props import (CollectionProperty, BoolProperty, EnumProperty, - IntProperty, StringProperty) -from bpy.types import PropertyGroup -from bl_operators.presets import AddPresetBase - -import vse_toolbox - -from vse_toolbox.constants import (ASSET_PREVIEWS, CASTING_BUFFER, CONFIG_DIR, - EDITS, EDIT_SUFFIXES, MOVIES, MOVIE_SUFFIXES, PREVIEWS_DIR, SOUNDS, - SOUND_SUFFIXES, TASK_ITEMS) - -from vse_toolbox.sequencer_utils import (clean_sequencer, get_strips, set_active_strip, - import_edit, import_movie, import_sound, rename_strips, render_strips, set_channels, - get_channel_index, new_text_strip, get_strip_render_path, get_strip_sequence_name) - -from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings, get_strip_settings) -from vse_toolbox.file_utils import (install_module, norm_name, norm_str, open_file, read_file) - - -class VSETB_OT_tracker_connect(Operator): - bl_idname = "vse_toolbox.tracker_connect" - bl_label = "Connect to Tracker" - bl_description = "Connect to Tracker" - - @classmethod - def poll(cls, context): - prefs = get_addon_prefs() - if prefs.tracker: - - return True - - def execute(self, context): - prefs = get_addon_prefs() - settings = get_scene_settings() - try: - prefs.tracker.connect() - self.report({'INFO'}, f'Sucessfully login to {settings.tracker_name.title()}') - return {"FINISHED"} - except Exception as e: - print('e: ', e) - self.report({'ERROR'}, f'Cannot connect to tracker.') - return {"CANCELLED"} - - -class VSETB_OT_copy_metadata(Operator): - bl_idname = "vse_toolbox.copy_metadata" - bl_label = "Copy metadata to selected" - bl_description = "Copy Metadata to selected strips" - - metadata : StringProperty() - - @classmethod - def poll(cls, context): - return context.selected_sequences and context.active_sequence_strip - - def execute(self, context): - prefs = get_addon_prefs() - settings = get_scene_settings() - project = settings.active_project - - metadata = next((m.field_name for m in project.metadata_types if m.name == self.metadata), None) - - if not metadata: - self.report({'ERROR'}, f'No Metadata named {self.metadata}') - - active_strip = context.active_sequence_strip - metadata_value = getattr(active_strip.vsetb_strip_settings.metadata, metadata) - - for strip in context.selected_sequences: - if strip == context.active_sequence_strip: - continue - - setattr(strip.vsetb_strip_settings.metadata, metadata, metadata_value) - - return {"FINISHED"} - - -class VSETB_OT_export_spreadsheet(Operator): - bl_idname = "vse_toolbox.export_spreadsheet" - bl_label = "Export Spreadsheet" - bl_description = "Export Shot data in a table as a csv or an xlsl" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context): - settings = get_scene_settings() - return settings.active_project - - def invoke(self, context, event): - settings = get_scene_settings() - project = settings.active_project - - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - scn = context.scene - settings = get_scene_settings() - project = settings.active_project - options = project.spreadsheet_options - - layout = self.layout - - row = layout.row() - row.template_list("VSETB_UL_spreadsheet", "spreadsheet", project, "spreadsheet", project, "spreadsheet_index", rows=8) - - col_tool = row.column(align=True) - - #bpy.types.VSETB_PT_presets.draw_panel_header(col_tool) - #col_tool.operator('wm.call_menu', icon="PRESET").name = 'VSETB_MT_spreadsheet_presets' - #col_tool.operator('vse_toolbox.load_spreadsheet_preset', icon='PRESET', text="") - op = col_tool.operator('wm.call_panel', icon="PRESET", emboss=False, text='') - op.name = 'VSETB_PT_presets' - op.keep_open = False - - col_tool.separator() - col_tool.operator('vse_toolbox.spreadsheet_move', icon='TRIA_UP', text="").direction = 'UP' - col_tool.operator('vse_toolbox.spreadsheet_move', icon='TRIA_DOWN', text="").direction = 'DOWN' - - col = layout.column() - col.use_property_split = True - - row = col.row(align=True, heading='Custom Name') - #row.use_property_split = True - row.prop(options, 'use_custom_name', text='') - sub = row.row(align=True) - sub.enabled = options.use_custom_name - sub.prop(options, 'custom_name', text='') - - col.separator() - - row = col.row(align=False) - row.prop(options, "format", expand=True, text='Format') - row.prop(options, 'show_settings', text='', icon='PREFERENCES') - if options.show_settings: - col.prop(options, "separator", expand=True, text='Separator') - if options.format == 'CSV': - col.prop(options, "delimiter", expand=True, text='Delimiter') - - col.separator() - col.prop(options, 'open_folder', text='Open Folder') - col.prop(options, 'export_path', text='Export Path') - - def execute(self, context): - #self.report({'ERROR'}, f'Export not implemented yet.') - prefs = get_addon_prefs() - settings = get_scene_settings() - project = settings.active_project - options = project.spreadsheet_options - episode = settings.active_episode - - cells = [cell for cell in project.spreadsheet if cell.enabled] - rows = [] - - # Header - rows.append([cell.export_name for cell in cells]) - - separator = options.separator.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r') - delimiter = options.delimiter.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r') - - for strip in get_strips('Shots'): - row = [] - for cell in cells: - if cell.type == "METADATA": - row += [getattr(strip.vsetb_strip_settings.metadata, cell.field_name)] - elif cell.type == "ASSET_TYPE": - asset_castings = [] - for asset_casting in strip.vsetb_strip_settings.casting: - asset = asset_casting.asset - if not asset.asset_type == cell.name: - continue - - if options.use_custom_name: - if asset.get('metadata', {}).get(options.custom_name): - asset_castings.append(asset['metadata'][options.custom_name]) - else: - self.report({'ERROR'}, f'The asset {asset.tracker_name} has no data {options.custom_name}') - else: - asset_castings.append(asset.tracker_name) - - row += [separator.join(asset_castings)] - elif cell.field_name == 'EPISODE': - row += [settings.active_episode.name] - elif cell.field_name == 'SEQUENCE': - row += [get_strip_sequence_name(strip)] - elif cell.field_name == 'SHOT': - row += [strip.name] - elif cell.field_name == 'DESCRIPTION': - row += [strip.vsetb_strip_settings.description] - elif cell.field_name == 'FRAMES': - row += [strip.frame_final_duration] - - rows.append(row) - - #print(rows) - - export_path = Path(os.path.abspath(bpy.path.abspath(options.export_path))) - export_name = export_path.name - - if export_path.suffix or export_name.endswith('{ext}'): - export_path = export_path.parent - - else: # It's a directory - if project.type == 'TVSHOW': - export_name = '{date}_{project}_{episode}_{tracker}_shots.{ext}' - else: - export_name = '{date}_{project}_{tracker}_shots.{ext}' - - date = datetime.now().strftime('%Y_%m_%d') - project_name = project.name.replace(' ', '_').lower() - episode_name = episode.name.replace(' ', '_').lower() if episode else 'episode' - ext = options.format.lower() - - export_name = export_name.format(date=date, project=project_name, - episode=episode_name, tracker=settings.tracker_name.lower(), ext=ext) - - export_path = export_path / export_name - - #2023_04_11_kitsu_boris_ep01_shots - export_path.parent.mkdir(parents=True, exist_ok=True) - - if options.format == 'CSV': - print('Writing .csv file to', export_path) - with open(str(export_path), 'w', newline='\n', encoding='utf-8') as f: - writer = csv.writer(f, delimiter=options.delimiter) - for row in rows: - writer.writerow(row) - - elif options.format == 'XLSX': - try: - import openpyxl - except ModuleNotFoundError: - self.report({'INFO'}, 'Installing openpyxl') - openpyxl = install_module('openpyxl') - - from openpyxl import Workbook - - workbook = Workbook() - worksheet = workbook.active - for row in rows: - worksheet.append(row) - - for col in worksheet.columns: - letter = col[0].column_letter - worksheet.column_dimensions[letter].auto_size = True - - # Save the file - workbook.save(str(export_path)) - - if options.open_folder: - open_file(export_path, select=True) - - - return {"FINISHED"} - - -def get_task_status_items(self, context): - settings = get_scene_settings() - project = settings.active_project - - status_items = [('CURRENT', 'Current', '')] - - if project: - status_items += [(t.name, t.name, '') for t in project.task_statuses] - return status_items - -def get_task_type_items(self, context): - settings = get_scene_settings() - project = settings.active_project - - if not project: - return [('NONE', 'None', '')] - - return [(t.name, t.name, '') for t in project.task_types] - -class VSETB_OT_upload_to_tracker(Operator): - bl_idname = "vse_toolbox.upload_to_tracker" - bl_label = "Upload to tracker" - bl_description = "Upload selected strip to tracker" - bl_options = {"REGISTER", "UNDO"} - - task : EnumProperty(items=get_task_type_items) - status : EnumProperty(items=get_task_status_items) - comment : StringProperty() - add_preview : BoolProperty(default=True) - preview_mode : EnumProperty(items=[(m, m.title().replace('_', ' '), '') - for m in ('ONLY_NEW', 'REPLACE', 'ADD')]) - set_main_preview : BoolProperty(default=True) - casting : BoolProperty(default=True) - custom_data : BoolProperty(default=True) - - @classmethod - def poll(cls, context): - return True - - def invoke(self, context, event): - prefs = get_addon_prefs() - settings = get_scene_settings() - project = settings.active_project - - tracker = prefs.tracker - tracker.connect() - - #self.bl_label = f"Upload to {settings.tracker_name.title()}" - - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - scn = context.scene - settings = get_scene_settings() - - layout = self.layout - col = layout.column() - col.use_property_split = True - col.prop(self, 'task', text='Task') - col.prop(self, 'status', text='Status') - col.prop(self, 'comment', text='Comment') - row = col.row(heading='Add Preview') - row.prop(self, 'add_preview', text='') - row.prop(self, 'preview_mode', text='') - col.separator() - col.prop(self, 'casting', text='Casting') - col.prop(self, 'custom_data', text='Custom Data') - - def execute(self, context): - #self.report({'ERROR'}, f'Export not implemented yet.') - prefs = get_addon_prefs() - settings = get_scene_settings() - project = settings.active_project - episode = None - if settings.active_episode: - episode = settings.active_episode.id - - - tracker = prefs.tracker - - status = self.status - if status == 'CURRENT': - status = None - - for strip in get_strips(channel='Shots', selected_only=True): - sequence_name = get_strip_sequence_name(strip) - shot_name = strip.name - sequence = tracker.get_sequence(sequence_name, episode=episode) - metadata = strip.vsetb_strip_settings.metadata.to_dict() - #print(metadata) - - if not sequence: - self.report({"INFO"}, f'Create sequence {sequence_name} in Kitsu') - sequence = tracker.new_sequence(sequence_name, episode=episode) - - shot = tracker.get_shot(shot_name, sequence=sequence) - if not shot: - self.report({"INFO"}, f'Create shot {shot_name} in Kitsu') - shot = tracker.new_shot(shot_name, sequence=sequence) - - task = tracker.get_task(self.task, entity=shot) - if not task: - task = tracker.new_task(shot, task_type=self.task) - - # print('\n', 'task comment') - # print(task['last_comment']) - - preview = None - if self.add_preview: - preview = Path(get_strip_render_path(strip, project.render_template)) - #print(preview) - if not preview.exists(): - print(f'The preview {preview} not exists') - preview = None - - elif task.get('last_comment') and task['last_comment']['previews']: - if self.preview_mode == 'REPLACE': - tracker.remove_comment(task['last_comment']) - elif self.preview_mode == 'ONLY_NEW': - preview = None - - #print(f'{preview=}') - #print(f'{status=}') - if status or preview: - tracker.new_comment(task, comment=self.comment, status=status, preview=preview, set_main_preview=self.set_main_preview) - - if self.custom_data: - metadata = strip.vsetb_strip_settings.metadata.to_dict() - description = strip.vsetb_strip_settings.description - tracker.update_data(shot, metadata, frames=strip.frame_final_duration, description=description) - - if self.casting: - casting = [{'asset_id': a.id, 'nb_occurences': a.instance} for a in strip.vsetb_strip_settings.casting] - tracker.update_casting(shot, casting) - - - return {"FINISHED"} - - -class VSETB_OT_auto_select_files(Operator): - bl_idname = "import.auto_select_files" - bl_label = "Auto Select" - bl_description = "Auto Select Files" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context): - return True - - def get_items(self, items=[]): - if not items: - return [('NONE', 'None', '', 0)] - return [(e, e, '', i) for i, e in enumerate(sorted(items))] - - def execute(self, context): - params = context.space_data.params - directory = Path(params.directory.decode()) - - EDITS.clear() - MOVIES.clear() - SOUNDS.clear() - - edits = [] - movies = [] - sounds = [] - - for file_entry in directory.glob('*'): - if file_entry.is_dir(): - continue - - if file_entry.suffix in EDIT_SUFFIXES: - edits.append(file_entry.name) - elif file_entry.suffix in MOVIE_SUFFIXES: - movies.append(file_entry.name) - elif file_entry.suffix in SOUND_SUFFIXES: - sounds.append(file_entry.name) - - EDITS.extend(self.get_items(items=edits)) - MOVIES.extend(self.get_items(items=movies)) - SOUNDS.extend(self.get_items(items=sounds)) - - return {'FINISHED'} - - -class VSETB_OT_import_files(Operator): - bl_idname = "vse_toolbox.import_files" - bl_label = "Import" - bl_description = "Import Edit" - bl_options = {"REGISTER", "UNDO"} - - directory : StringProperty(subtype='DIR_PATH') - filepath: StringProperty( - name="File Path", - description="Filepath used for importing the file", - maxlen=1024, - subtype='FILE_PATH', - ) - files : CollectionProperty(type=bpy.types.OperatorFileListElement) - clean_sequencer : BoolProperty( - name="Clean Sequencer", - default=False, - description="Clean all existing strips in sequencer", - ) - - import_edit : BoolProperty(name='', default=True) - edit: EnumProperty(name='', items=lambda s, c: EDITS) - - import_movie : BoolProperty(name='', default=False) - movie: EnumProperty(name='', items=lambda s, c: MOVIES) - - import_sound : BoolProperty(name='', default=False) - sound: EnumProperty(name='', items=lambda s, c: SOUNDS) - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - scn = context.scene - settings = get_scene_settings() - - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - - col = layout.column(align=True) - col.operator('import.auto_select_files', text='Auto Select') - - row = self.layout.row(heading="Import Edit", align=True) - row.prop(self, 'import_edit') - sub = row.row(align=True) - sub.active = self.import_edit - sub.prop(self, 'edit') - - row = self.layout.row(heading="Import Movie", align=True) - row.prop(self, 'import_movie') - sub = row.row() - sub.active = self.import_movie - sub.prop(self, 'movie') - - row = self.layout.row(heading="Import Sound", align=True) - row.prop(self, 'import_sound') - sub = row.row() - sub.active = self.import_sound - sub.prop(self, 'sound') - - col = layout.column() - col.separator() - col.prop(self, 'clean_sequencer') - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - def execute(self, context): - otio = install_module('opentimelineio') - - edit_filepath = Path(self.directory, self.edit) - if not edit_filepath.exists(): - self.import_edit = False - - movie_filepath = Path(self.directory, self.movie) - if not movie_filepath.exists(): - self.import_movie = False - - sound_filepath = Path(self.directory, self.sound) - if not sound_filepath.exists(): - self.import_sound = False - - if self.clean_sequencer: - clean_sequencer( - edit=self.import_edit, - movie=self.import_movie, - sound=self.import_sound, - ) - - if self.import_edit: - print(f'[>.] Loading Edit from: {str(edit_filepath)}') - import_edit(edit_filepath, adapter="cmx_3600") - - if self.import_movie: - print(f'[>.] Loading Movie from: {str(movie_filepath)}') - import_movie(movie_filepath) - - if self.import_sound: - print(f'[>.] Loading Audio from: {str(sound_filepath)}') - import_sound(sound_filepath) - elif not self.import_sound and self.import_movie: - print(f'[>.] Loading Audio from Movie: {str(movie_filepath)}') - import_sound(movie_filepath) - - context.scene.sequence_editor.sequences.update() - - return {"FINISHED"} - - -class VSETB_OT_load_assets(Operator): - bl_idname = "vse_toolbox.load_assets" - bl_label = "Load Assets for current projects" - bl_description = "Load Assets" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context): - settings = get_scene_settings() - if settings.active_project: - return True - - def execute(self, context): - settings = get_scene_settings() - prefs = get_addon_prefs() - tracker = prefs.tracker - - tracker.connect() - project = settings.active_project - project.assets.clear() - - assets = tracker.get_assets(project['id']) - if not assets: - self.report({'ERROR'}, f'No Assets found for {project.name}.') - - for asset_data in assets: - asset = project.assets.add() - - asset.name = asset_data['id'] - asset.norm_name = norm_name(asset_data['name'], separator=' ', format=str.lower) - - asset.tracker_name = asset_data['name'] - asset.id = asset_data['id'] - asset.asset_type = asset_data['asset_type'] - - #for key, value in asset_data.get('data', {}).items(): - asset['metadata'] = asset_data.get('data', {}) - - preview_id = asset_data.get('preview_file_id') - if preview_id: - asset.preview = preview_id - preview_path = Path(PREVIEWS_DIR / project.id / preview_id).with_suffix('.png') - tracker.download_preview(preview_id, preview_path) - - if preview_id not in ASSET_PREVIEWS: - ASSET_PREVIEWS.load(preview_id, preview_path.as_posix(), 'IMAGE', True) - - - self.report({'INFO'}, f'Assets for {project.name} successfully loaded') - return {"FINISHED"} - - -class VSETB_OT_load_projects(Operator): - bl_idname = "vse_toolbox.load_projects" - bl_label = "Load Projects" - bl_description = "Load Projects" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - settings = get_scene_settings() - prefs = get_addon_prefs() - tracker = prefs.tracker - - old_project_name = settings.project_name.replace(' ', '_').upper() - - settings.projects.clear() - tracker.connect() - - for project_data in tracker.get_projects(): - project = settings.projects.add() - project.type = project_data['production_type'].upper().replace(' ', '') - project.name = project_data['name'] - project.id = project_data['id'] - - if project.type == 'TVSHOW': - for episode_data in tracker.get_episodes(project_data): - episode = project.episodes.add() - episode.name = episode_data['name'] - episode.id = episode_data['id'] - - for metadata_data in tracker.get_metadata_types(project_data): - #pprint(metadata_data) - metadata_type = project.metadata_types.add() - metadata_type.name = metadata_data['name'] - metadata_type.field_name = metadata_data['field_name'] - #metadata_type['choices'] = metadata_data['choices'] - - if prefs.sort_metadata_items: - metadata_data['choices'].sort() - - for choice in metadata_data['choices']: - choice_item = metadata_type.choices.add() - choice_item.name = choice - - metadata_type['entity_type'] = metadata_data['entity_type'].upper() - - for status_data in tracker.get_task_statuses(project_data): - #print(metadata_data) - task_status = project.task_statuses.add() - task_status.name = status_data['short_name'].upper() - - for task_type_data in tracker.get_shot_task_types(project_data): - task_type = project.task_types.add() - task_type.name = task_type_data['name'] - - for asset_type_data in tracker.get_asset_types(project_data): - asset_type = project.asset_types.add() - asset_type.name = asset_type_data['name'] - - project.set_spreadsheet() - - for project in settings.projects: - if project.name.replace(' ', '_').upper() == old_project_name: - settings.project_name = project.name - - load_settings() - #else: - # print('Could Not restore Project Name') - - #if settings.active_project: - # settings.active_project.set_strip_metadata() - - #bpy.ops.vse_toolbox.load_assets() - - self.report({"INFO"}, 'Successfully Load Tracker Projects') - - return {'FINISHED'} - - -class VSETB_OT_new_episode(Operator): - bl_idname = "vse_toolbox.new_episode" - bl_label = "New Epispde" - bl_description = "Add new Episode to Project" - bl_options = {"REGISTER", "UNDO"} - - episode_name : StringProperty(name="Episode Name", default="") - - @classmethod - def poll(cls, context): - return True - - def invoke(self, context, event): - scn = context.scene - settings = get_scene_settings() - - return context.window_manager.invoke_props_dialog(self) - - def execute(self, context): - settings = get_scene_settings() - prefs = get_addon_prefs() - tracker = prefs.tracker - - episode_name = settings.episode_template.format(index=int(self.episode_name)) - - #print(self.episode_name) - #print('episode_name: ', episode_name) - - episode = tracker.get_episode(episode_name) - if episode: - self.report({'ERROR'}, f'Episode {episode_name} already exists') - return {"CANCELLED"} - - tracker.new_episode(episode_name) - # tracker.get_episodes - tracker.update_project() - self.report({'INFO'}, f'Episode {episode_name} successfully created') - return {'FINISHED'} - - -class VSETB_OT_reload_addon(Operator): - bl_idname = "vse_toolbox.reload_addon" - bl_options = {"UNDO"} - bl_label = 'Reload VSE ToolBox Addon' - bl_description = 'Reload The VSE ToolBox Addon' - - def execute(self, context): - - print('Execute reload') - - vse_toolbox.unregister() - importlib.reload(vse_toolbox) - vse_toolbox.register() - - return {'FINISHED'} - - -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) - channel_name : StringProperty(name="Channel Name", default="") - selected_only : BoolProperty(name="Selected Only", default=False) - #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() - return settings.active_project - - 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 - - col = layout.column() - col.use_property_split = True - col.use_property_decorate = False - - if self.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, 'reset_by_sequence') - elif self.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(self, 'selected_only') - - def execute(self, context): - scn = context.scene - settings = get_scene_settings() - project = settings.active_project - - strips = get_strips(channel=self.channel_name, selected_only=self.selected_only) - if self.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 - ) - - if self.channel_name == 'Sequences': - rename_strips(strips, - template=project.sequence_template, - increment=project.sequence_increment, start_number=project.sequence_start_number - ) - - return {"FINISHED"} - - -class VSETB_OT_render(Operator): - bl_idname = "vse_toolbox.strips_render" - bl_label = "Render Shots Strips" - bl_description = "Render Shots Strips" - bl_options = {"REGISTER", "UNDO"} - - #selected_only : BoolProperty(name="Selected Only", default=False) - - @classmethod - def poll(cls, context): - settings = get_scene_settings() - return settings.active_project - - def invoke(self, context, event): - scn = context.scene - settings = get_scene_settings() - - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - scn = context.scene - settings = get_scene_settings() - - layout = self.layout - col = layout.column() - col.use_property_split = True - col.use_property_decorate = False - #col.prop(settings, 'channel', text='Channel') - #col.prop(self, 'selected_only') - - col.prop(settings.active_project, "render_template") - - def execute(self, context): - scn = context.scene - settings = get_scene_settings() - strips = get_strips(channel='Shots', selected_only=True) - - start_time = time.perf_counter() - render_strips(strips, settings.active_project.render_template) - - self.report({"INFO"}, f'Strips rendered in {time.perf_counter()-start_time} seconds') - - 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() - movie = get_strips(channel='Movie') - if movie: - movie = movie[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 = 5 - 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 - - return {"FINISHED"} - - -class VSETB_OT_casting_replace(Operator): - bl_idname = "vse_toolbox.casting_replace" - bl_label = "Replace Asset" - bl_description = "Replace Asset of selected strips" - - old_asset : StringProperty() - new_asset : StringProperty() - - assets : CollectionProperty(type=PropertyGroup) - - def execute(self, context): - prefs = get_addon_prefs() - settings = get_scene_settings() - - new_asset = next(a for a in settings.active_project.assets if a.tracker_name == self.new_asset) - - for strip in get_strips('Shots', selected_only=True): - strip_settings = strip.vsetb_strip_settings - for asset_casting in strip_settings.casting: - if asset_casting.asset.tracker_name == self.old_asset: - - print(f'Replace casting on {strip.name}') - - asset_casting.name = new_asset.name - asset_casting.id = new_asset.id - asset_casting['_name'] = new_asset.label - - strip_settings.casting.update() - - self.assets.clear() - - return {'FINISHED'} - - def invoke(self, context, event): - settings = get_scene_settings() - project = settings.active_project - - self.assets.clear() - for asset in project.assets: - item = self.assets.add() - item.name = asset.tracker_name - - strip = context.active_sequence_strip - asset_casting_index = strip.vsetb_strip_settings.casting_index - active_asset = strip.vsetb_strip_settings.casting[asset_casting_index].asset - - self.old_asset = active_asset.tracker_name - self.new_asset = '' - - return context.window_manager.invoke_props_dialog(self) - # - - def draw(self, context): - scn = context.scene - settings = get_scene_settings() - - project = settings.active_project - - layout = self.layout - col = layout.column() - col.use_property_split = True - col.prop_search(self, 'old_asset', self, 'assets', text='Old Asset', icon='ASSET_MANAGER') - col.prop_search(self, 'new_asset', self, 'assets', text='New Asset', icon='ASSET_MANAGER') - - -def get_asset_items(self, context): - settings = get_scene_settings() - project = settings.active_project - - return [(a.id, a.label, '', i) for i, a in enumerate(project.assets)] - -class VSETB_OT_casting_add(Operator): - bl_idname = "vse_toolbox.casting_add" - bl_label = "Casting Add" - bl_description = "Add Asset to Castin" - bl_options = {"REGISTER", "UNDO"} - bl_property = "asset_name" - - asset_name : EnumProperty(name='', items=get_asset_items) - @classmethod - def poll(cls, context): - active_strip = context.scene.sequence_editor.active_strip - if active_strip: - return True - - def invoke(self, context, event): - context.window_manager.invoke_search_popup(self) - return {'FINISHED'} - - def execute(self, context): - scn = context.scene - active_strip = scn.sequence_editor.active_strip - - settings = get_scene_settings() - strip_settings = get_strip_settings() - - project = settings.active_project - - if strip_settings.casting.get(self.asset_name): - asset = project.assets[self.asset_name] - self.report({'WARNING'}, f"Asset {asset.label} already casted.") - return {"CANCELLED"} - - item = strip_settings.casting.add() - asset = project.assets[self.asset_name] - - item.name = asset.name - item.id = project.assets[self.asset_name].id - item['_name'] = asset.label - - strip_settings.casting.update() - - return {"FINISHED"} - - -class VSETB_OT_casting_remove(Operator): - bl_idname = "vse_toolbox.casting_remove" - bl_label = "Remove Item from Casting" - bl_description = "Remove Item from Casting" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context): - active_strip = context.scene.sequence_editor.active_strip - if active_strip: - return True - - def invoke(self, context, event): - scn = context.scene - - strip_settings = get_strip_settings() - idx = strip_settings.casting_index - - try: - item = strip_settings.casting[idx] - except IndexError: - pass - else: - item = strip_settings.casting[strip_settings.casting_index] - label = item.asset.label if item.asset.label else 'Empty' - info = f"Item {label} removed from casting" - strip_settings.casting.remove(idx) - - if strip_settings.casting_index == 0: - strip_settings.casting_index = 0 - else: - strip_settings.casting_index -= 1 - - self.report({'INFO'}, info) - - return {"FINISHED"} - - -class VSETB_OT_casting_move(Operator): - bl_idname = "vse_toolbox.casting_move" - bl_label = "Move Casting items" - bl_description = "Move Casting items" - bl_options = {"REGISTER", "UNDO"} - - direction: EnumProperty( - items=( - ('UP', "Up", ""), - ('DOWN', "Down", ""), - ) - ) - - asset_name : StringProperty() - - @classmethod - def poll(cls, context): - active_strip = context.scene.sequence_editor.active_strip - if active_strip: - return True - - def execute(self, context): - scn = context.scene - - strip_settings = get_strip_settings() - idx = strip_settings.casting_index - - try: - item = strip_settings.casting[idx] - except IndexError: - pass - else: - if self.direction == 'DOWN' and idx < len(strip_settings.casting) - 1: - item_next = strip_settings.casting[idx+1].name - strip_settings.casting.move(idx, idx+1) - strip_settings.casting_index += 1 - - elif self.direction == 'UP' and idx >= 1: - item_prev = strip_settings.casting[idx-1].name - strip_settings.casting.move(idx, idx-1) - strip_settings.casting_index -= 1 - - info = f"Item {item.asset.label} moved to position {(item.asset.label, strip_settings.casting_index + 1)}" - self.report({'INFO'}, info) - - return {"FINISHED"} - - -class VSETB_MT_spreadsheet_presets(Menu): - bl_label = 'Presets' - preset_subdir = 'vse_toolbox' - preset_operator = 'script.execute_preset' - draw = Menu.draw_preset - - -class VSETB_OT_add_spreadsheet_preset(AddPresetBase, Operator): - - bl_idname = "vse_toolbox.add_spreadsheet_preset" - bl_label = "Add Spreadsheet Preset" - bl_description = "Add Spreadsheet Preset" - bl_options = {"REGISTER", "UNDO"} - - preset_menu = 'VSETB_MT_spreadsheet_presets' - - #preset_menu = 'VSETB_OT_MT_spreadsheet_presets' - - # Common variable used for all preset values - #C.scene.vsetb_settings.active_project.spreadsheet_options - preset_defines = [ - 'scene = bpy.context.scene', - 'settings = scene.vsetb_settings', - 'project = settings.active_project' - ] - - # Properties to store in the preset - preset_values = [ - 'project.spreadsheet', - 'project.spreadsheet_options' - ] - - # Directory to store the presets - preset_subdir = 'vse_toolbox' - - - -class VSETB_OT_spreadsheet_move(Operator): - bl_idname = "vse_toolbox.spreadsheet_move" - bl_label = "Move Spreadsheet items" - bl_description = "Move Spreadsheet items" - bl_options = {"REGISTER", "UNDO"} - - direction: EnumProperty( - items=( - ('UP', "Up", ""), - ('DOWN', "Down", ""), - ) - ) - - def execute(self, context): - scn = context.scene - project = get_scene_settings().active_project - - idx = project.spreadsheet_index - - try: - item = project.spreadsheet[idx] - except IndexError: - pass - else: - if self.direction == 'DOWN' and idx < len(project.spreadsheet) - 1: - item_next = project.spreadsheet[idx+1].name - project.spreadsheet.move(idx, idx+1) - project.spreadsheet_index += 1 - - elif self.direction == 'UP' and idx >= 1: - item_prev = project.spreadsheet[idx-1].name - project.spreadsheet.move(idx, idx-1) - project.spreadsheet_index -= 1 - - return {"FINISHED"} - - -class VSETB_OT_copy_casting(Operator): - bl_idname = "vse_toolbox.copy_casting" - bl_label = "Copy Casting" - bl_description = "Copy Casting from active strip" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context): - active_strip = context.scene.sequence_editor.active_strip - strip_settings = get_strip_settings() - - if active_strip and strip_settings.casting: - return True - - def execute(self, context): - scn = context.scene - - active_strip = scn.sequence_editor.active_strip - strip_settings = get_strip_settings() - - datas = [c.to_dict() for c in strip_settings.casting] - CASTING_BUFFER.write_text(json.dumps(datas), encoding='utf-8') - - return {"FINISHED"} - - -class VSETB_OT_paste_casting(Operator): - bl_idname = "vse_toolbox.paste_casting" - bl_label = "Paste Casting" - bl_description = "Paste Casting to active strip" - bl_options = {"REGISTER", "UNDO"} - - @classmethod - def poll(cls, context): - active_strip = context.scene.sequence_editor.active_strip - if active_strip: - return True - - def execute(self, context): - scn = context.scene - strip_settings = get_strip_settings() - - if not CASTING_BUFFER.exists(): - self.report({'ERROR'}, f'No Casting to copy.') - return {"CANCELLED"} - - casting_datas = json.loads(CASTING_BUFFER.read_text()) - - - - for strip in context.selected_sequences: - strip_settings = strip.vsetb_strip_settings - for casting_data in casting_datas: - - item = strip.vsetb_strip_settings.casting.add() - - item.name = casting_data['name'] - item.id = casting_data['id'] - item['_name'] = casting_data['_name'] - - strip_settings.casting.update() - - 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() - #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') - - crop_x = int(scn.render.resolution_x * 0.4) - crop_max_y = int(scn.render.resolution_y * 0.96) - crop_min_y = int(scn.render.resolution_y * 0.01) - - stamp_params = dict(start=scn.frame_start, end=scn.frame_end, - font_size=22, y=0.015, box_margin=0.005, select=True, box_color=(0, 0, 0, 0.5)) - - # Project Name - project_strip_stamp = new_text_strip('project_name_stamp', channel=1, **stamp_params, - text=settings.active_project.name, 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 - project_strip_stamp.crop.min_y = crop_min_y - - # Shot Name - shot_strip_stamp = new_text_strip('shot_name_stamp', channel=2, **stamp_params, - text='{active_shot_name}', 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 - shot_strip_stamp.crop.min_y = crop_min_y - - # Frame Range - frame_strip_stamp = new_text_strip('frame_range_stamp', channel=3, **stamp_params, - text='{active_shot_frame} / {active_shot_duration}', 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 - frame_strip_stamp.crop.min_y = crop_min_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"} - - -def load_settings(): - prefs = get_addon_prefs() - settings = get_scene_settings() - project = settings.active_project - - prefs_config_file = prefs.config_path - - if not prefs_config_file: - return - - prefs_datas = read_file(os.path.expandvars(prefs_config_file)) - - prefs_datas['trackers'] = prefs_datas.get('trackers') - prefs_datas['spreadsheet'] = prefs_datas.get('spreadsheet') - - #print(prefs_datas) - trackers = prefs_datas.pop('trackers') - spreadsheet = prefs_datas.pop('spreadsheet') - - project_name = prefs_datas.get('project_name') - if project_name: - settings.project_name = project_name - - # Project Properties - for k, v in prefs_datas.items(): - try: - setattr(project, k, v) - except Exception: - print(f'Could not set property {k} with value {v} to project {settings.project_name}') - - if spreadsheet.get('cells'): - #project.spreadsheet.clear() - - for i, cell_data in enumerate(spreadsheet['cells']): - if not 'name' in cell_data: - print(f'cell_data {cell_data} need to have a attribute name') - continue - - cell = project.spreadsheet.get(cell_data['name']) - - if not cell: - print(f"cell {cell_data['name']} not in spreadsheet") - continue - - project.spreadsheet.move(list(project.spreadsheet).index(cell), i) - - for prop_name in ('export_name', 'enabled'): - if prop_name in cell_data: - setattr(cell, prop_name, cell_data[prop_name]) - - if spreadsheet.get('options'): - for k, v in spreadsheet['options'].items(): - try: - setattr(project.spreadsheet_options, k, v) - except Exception: - print(f'Could not set option {k} with value {v} to spreadsheet') - -classes = ( - VSETB_OT_auto_select_files, - VSETB_OT_casting_add, - VSETB_OT_casting_remove, - VSETB_OT_casting_move, - VSETB_OT_add_spreadsheet_preset, - VSETB_MT_spreadsheet_presets, - VSETB_OT_spreadsheet_move, - VSETB_OT_copy_casting, - VSETB_OT_paste_casting, - VSETB_OT_casting_replace, - VSETB_OT_export_spreadsheet, - VSETB_OT_import_files, - VSETB_OT_load_assets, - VSETB_OT_load_projects, - VSETB_OT_new_episode, - VSETB_OT_reload_addon, - VSETB_OT_rename, - VSETB_OT_render, - VSETB_OT_set_sequencer, - VSETB_OT_tracker_connect, - VSETB_OT_set_stamps, - VSETB_OT_upload_to_tracker, - VSETB_OT_show_waveform, - VSETB_OT_copy_metadata -) - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/render.py b/operators/render.py new file mode 100644 index 0000000..ad20331 --- /dev/null +++ b/operators/render.py @@ -0,0 +1,70 @@ + +import time + +import bpy +from bpy.types import Operator + +from vse_toolbox.sequencer_utils import (get_strips, render_strips) +from vse_toolbox.bl_utils import get_scene_settings + + + +class VSETB_OT_render(Operator): + bl_idname = "vse_toolbox.strips_render" + bl_label = "Render Shots Strips" + bl_description = "Render Shots Strips" + bl_options = {"REGISTER", "UNDO"} + + #selected_only : BoolProperty(name="Selected Only", default=False) + + @classmethod + def poll(cls, context): + settings = get_scene_settings() + return settings.active_project + + def invoke(self, context, event): + scn = context.scene + settings = get_scene_settings() + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + scn = context.scene + settings = get_scene_settings() + + layout = self.layout + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + #col.prop(settings, 'channel', text='Channel') + #col.prop(self, 'selected_only') + + col.prop(settings.active_project, "render_template") + + def execute(self, context): + scn = context.scene + settings = get_scene_settings() + strips = get_strips(channel='Shots', selected_only=True) + + start_time = time.perf_counter() + render_strips(strips, settings.active_project.render_template) + + self.report({"INFO"}, f'Strips rendered in {time.perf_counter()-start_time} seconds') + + return {"FINISHED"} + + + + + +classes = ( + VSETB_OT_render, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/sequencer.py b/operators/sequencer.py new file mode 100644 index 0000000..fdff2bd --- /dev/null +++ b/operators/sequencer.py @@ -0,0 +1,216 @@ + +import bpy +from bpy.types import Operator +from bpy.props import (BoolProperty, StringProperty) + +from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels, + get_channel_index, new_text_strip) + +from vse_toolbox.bl_utils import get_scene_settings + + +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) + channel_name : StringProperty(name="Channel Name", default="") + selected_only : BoolProperty(name="Selected Only", default=False) + #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() + return settings.active_project + + 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 + + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + + if self.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, 'reset_by_sequence') + elif self.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(self, 'selected_only') + + def execute(self, context): + scn = context.scene + settings = get_scene_settings() + project = settings.active_project + + strips = get_strips(channel=self.channel_name, selected_only=self.selected_only) + if self.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 + ) + + if self.channel_name == 'Sequences': + rename_strips(strips, + template=project.sequence_template, + increment=project.sequence_increment, start_number=project.sequence_start_number + ) + + 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() + movie = get_strips(channel='Movie') + if movie: + movie = movie[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 = 5 + 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 + + 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() + #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') + + crop_x = int(scn.render.resolution_x * 0.4) + crop_max_y = int(scn.render.resolution_y * 0.96) + crop_min_y = int(scn.render.resolution_y * 0.01) + + stamp_params = dict(start=scn.frame_start, end=scn.frame_end, + font_size=22, y=0.015, box_margin=0.005, select=True, box_color=(0, 0, 0, 0.5)) + + # Project Name + project_strip_stamp = new_text_strip('project_name_stamp', channel=1, **stamp_params, + text=settings.active_project.name, 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 + project_strip_stamp.crop.min_y = crop_min_y + + # Shot Name + shot_strip_stamp = new_text_strip('shot_name_stamp', channel=2, **stamp_params, + text='{active_shot_name}', 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 + shot_strip_stamp.crop.min_y = crop_min_y + + # Frame Range + frame_strip_stamp = new_text_strip('frame_range_stamp', channel=3, **stamp_params, + text='{active_shot_frame} / {active_shot_duration}', 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 + frame_strip_stamp.crop.min_y = crop_min_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"} + + +classes = ( + VSETB_OT_rename, + VSETB_OT_set_sequencer, + VSETB_OT_set_stamps, + VSETB_OT_show_waveform, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/spreadsheet.py b/operators/spreadsheet.py new file mode 100644 index 0000000..cbe316a --- /dev/null +++ b/operators/spreadsheet.py @@ -0,0 +1,319 @@ + +# SPDX-License-Identifier: GPL-2.0-or-later + +import csv +from datetime import datetime +import os +from pathlib import Path + +import bpy +from bpy.types import (Operator, Menu, OperatorFileListElement) +from bpy.props import (EnumProperty, ) +from bl_operators.presets import AddPresetBase + + +from vse_toolbox.sequencer_utils import (get_strips, get_strip_sequence_name) +from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings) +from vse_toolbox.file_utils import open_file + + +class VSETB_MT_spreadsheet_presets(Menu): + bl_label = 'Presets' + preset_subdir = 'vse_toolbox' + preset_operator = 'script.execute_preset' + draw = Menu.draw_preset + + +class VSETB_OT_add_spreadsheet_preset(AddPresetBase, Operator): + + bl_idname = "vse_toolbox.add_spreadsheet_preset" + bl_label = "Add Spreadsheet Preset" + bl_description = "Add Spreadsheet Preset" + bl_options = {"REGISTER", "UNDO"} + + preset_menu = 'VSETB_MT_spreadsheet_presets' + + #preset_menu = 'VSETB_OT_MT_spreadsheet_presets' + + # Common variable used for all preset values + #C.scene.vsetb_settings.active_project.spreadsheet_options + preset_defines = [ + 'scene = bpy.context.scene', + 'settings = scene.vsetb_settings', + 'project = settings.active_project' + ] + + # Properties to store in the preset + preset_values = [ + 'project.spreadsheet', + 'project.spreadsheet_options' + ] + + # Directory to store the presets + preset_subdir = 'vse_toolbox' + + + +class VSETB_OT_spreadsheet_move(Operator): + bl_idname = "vse_toolbox.spreadsheet_move" + bl_label = "Move Spreadsheet items" + bl_description = "Move Spreadsheet items" + bl_options = {"REGISTER", "UNDO"} + + direction: EnumProperty( + items=( + ('UP', "Up", ""), + ('DOWN', "Down", ""), + ) + ) + + def execute(self, context): + scn = context.scene + project = get_scene_settings().active_project + + idx = project.spreadsheet_index + + try: + item = project.spreadsheet[idx] + except IndexError: + pass + else: + if self.direction == 'DOWN' and idx < len(project.spreadsheet) - 1: + item_next = project.spreadsheet[idx+1].name + project.spreadsheet.move(idx, idx+1) + project.spreadsheet_index += 1 + + elif self.direction == 'UP' and idx >= 1: + item_prev = project.spreadsheet[idx-1].name + project.spreadsheet.move(idx, idx-1) + project.spreadsheet_index -= 1 + + return {"FINISHED"} + + +class VSETB_OT_export_spreadsheet(Operator): + bl_idname = "vse_toolbox.export_spreadsheet" + bl_label = "Export Spreadsheet" + bl_description = "Export Shot data in a table as a csv or an xlsl" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + settings = get_scene_settings() + return settings.active_project + + def invoke(self, context, event): + settings = get_scene_settings() + project = settings.active_project + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + scn = context.scene + settings = get_scene_settings() + project = settings.active_project + options = project.spreadsheet_options + + layout = self.layout + + row = layout.row() + row.template_list("VSETB_UL_spreadsheet", "spreadsheet", project, "spreadsheet", project, "spreadsheet_index", rows=8) + + col_tool = row.column(align=True) + + #bpy.types.VSETB_PT_presets.draw_panel_header(col_tool) + #col_tool.operator('wm.call_menu', icon="PRESET").name = 'VSETB_MT_spreadsheet_presets' + #col_tool.operator('vse_toolbox.load_spreadsheet_preset', icon='PRESET', text="") + op = col_tool.operator('wm.call_panel', icon="PRESET", emboss=False, text='') + op.name = 'VSETB_PT_presets' + op.keep_open = False + + col_tool.separator() + col_tool.operator('vse_toolbox.spreadsheet_move', icon='TRIA_UP', text="").direction = 'UP' + col_tool.operator('vse_toolbox.spreadsheet_move', icon='TRIA_DOWN', text="").direction = 'DOWN' + + col = layout.column() + col.use_property_split = True + + row = col.row(align=True, heading='Custom Name') + #row.use_property_split = True + row.prop(options, 'use_custom_name', text='') + sub = row.row(align=True) + sub.enabled = options.use_custom_name + sub.prop(options, 'custom_name', text='') + + col.separator() + + row = col.row(align=False) + row.prop(options, "format", expand=True, text='Format') + row.prop(options, 'show_settings', text='', icon='PREFERENCES') + if options.show_settings: + col.prop(options, "separator", expand=True, text='Separator') + if options.format == 'CSV': + col.prop(options, "delimiter", expand=True, text='Delimiter') + + col.separator() + col.prop(options, 'open_folder', text='Open Folder') + col.prop(options, 'export_path', text='Export Path') + + def execute(self, context): + #self.report({'ERROR'}, f'Export not implemented yet.') + prefs = get_addon_prefs() + settings = get_scene_settings() + project = settings.active_project + options = project.spreadsheet_options + episode = settings.active_episode + + cells = [cell for cell in project.spreadsheet if cell.enabled] + rows = [] + + # Header + rows.append([cell.export_name for cell in cells]) + + separator = options.separator.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r') + delimiter = options.delimiter.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r') + + for strip in get_strips('Shots'): + row = [] + for cell in cells: + if cell.type == "METADATA": + row += [getattr(strip.vsetb_strip_settings.metadata, cell.field_name)] + elif cell.type == "ASSET_TYPE": + asset_castings = [] + for asset_casting in strip.vsetb_strip_settings.casting: + asset = asset_casting.asset + if not asset.asset_type == cell.name: + continue + + if options.use_custom_name: + if asset.get('metadata', {}).get(options.custom_name): + asset_castings.append(asset['metadata'][options.custom_name]) + else: + self.report({'ERROR'}, f'The asset {asset.tracker_name} has no data {options.custom_name}') + else: + asset_castings.append(asset.tracker_name) + + row += [separator.join(asset_castings)] + elif cell.field_name == 'EPISODE': + row += [settings.active_episode.name] + elif cell.field_name == 'SEQUENCE': + row += [get_strip_sequence_name(strip)] + elif cell.field_name == 'SHOT': + row += [strip.name] + elif cell.field_name == 'DESCRIPTION': + row += [strip.vsetb_strip_settings.description] + elif cell.field_name == 'FRAMES': + row += [strip.frame_final_duration] + + rows.append(row) + + #print(rows) + + export_path = Path(os.path.abspath(bpy.path.abspath(options.export_path))) + export_name = export_path.name + + if export_path.suffix or export_name.endswith('{ext}'): + export_path = export_path.parent + + else: # It's a directory + if project.type == 'TVSHOW': + export_name = '{date}_{project}_{episode}_{tracker}_shots.{ext}' + else: + export_name = '{date}_{project}_{tracker}_shots.{ext}' + + date = datetime.now().strftime('%Y_%m_%d') + project_name = project.name.replace(' ', '_').lower() + episode_name = episode.name.replace(' ', '_').lower() if episode else 'episode' + ext = options.format.lower() + + export_name = export_name.format(date=date, project=project_name, + episode=episode_name, tracker=settings.tracker_name.lower(), ext=ext) + + export_path = export_path / export_name + + #2023_04_11_kitsu_boris_ep01_shots + export_path.parent.mkdir(parents=True, exist_ok=True) + + if options.format == 'CSV': + print('Writing .csv file to', export_path) + with open(str(export_path), 'w', newline='\n', encoding='utf-8') as f: + writer = csv.writer(f, delimiter=options.delimiter) + for row in rows: + writer.writerow(row) + + elif options.format == 'XLSX': + try: + import openpyxl + except ModuleNotFoundError: + self.report({'INFO'}, 'Installing openpyxl') + openpyxl = install_module('openpyxl') + + from openpyxl import Workbook + + workbook = Workbook() + worksheet = workbook.active + for row in rows: + worksheet.append(row) + + for col in worksheet.columns: + letter = col[0].column_letter + worksheet.column_dimensions[letter].auto_size = True + + # Save the file + workbook.save(str(export_path)) + + if options.open_folder: + open_file(export_path, select=True) + + return {"FINISHED"} + + + +class VSETB_OT_import_spreadsheet(Operator): + bl_idname = "vse_toolbox.import_spreadsheet" + bl_label = "Import Spreadsheet" + bl_description = "Create strips from nb frames with casting and custom data" + bl_options = {"REGISTER", "UNDO"} + + directory : StringProperty(subtype='DIR_PATH') + filepath: StringProperty( + name="File Path", + description="Filepath used for importing the file", + maxlen=1024, + subtype='FILE_PATH', + ) + files : CollectionProperty(type=OperatorFileListElement) + + @classmethod + def poll(cls, context): + settings = get_scene_settings() + return settings.active_project + + def invoke(self, context, event): + settings = get_scene_settings() + project = settings.active_project + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + scn = context.scene + + def execute(self, context): + + return {"FINISHED"} + + +classes = ( + VSETB_OT_add_spreadsheet_preset, + VSETB_MT_spreadsheet_presets, + VSETB_OT_spreadsheet_move, + VSETB_OT_export_spreadsheet, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/operators/tracker.py b/operators/tracker.py new file mode 100644 index 0000000..d416331 --- /dev/null +++ b/operators/tracker.py @@ -0,0 +1,367 @@ + +from pathlib import Path + +import bpy +from bpy.types import (Operator, ) +from bpy.props import (BoolProperty, EnumProperty, StringProperty) + +from vse_toolbox.constants import (ASSET_PREVIEWS, PREVIEWS_DIR) + +from vse_toolbox.sequencer_utils import (get_strips, get_strip_render_path, get_strip_sequence_name) + +from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings) +from vse_toolbox.file_utils import (norm_name,) + + +class VSETB_OT_tracker_connect(Operator): + bl_idname = "vse_toolbox.tracker_connect" + bl_label = "Connect to Tracker" + bl_description = "Connect to Tracker" + + @classmethod + def poll(cls, context): + prefs = get_addon_prefs() + if prefs.tracker: + + return True + + def execute(self, context): + prefs = get_addon_prefs() + settings = get_scene_settings() + try: + prefs.tracker.connect() + self.report({'INFO'}, f'Sucessfully login to {settings.tracker_name.title()}') + return {"FINISHED"} + except Exception as e: + print('e: ', e) + self.report({'ERROR'}, f'Cannot connect to tracker.') + return {"CANCELLED"} + + +class VSETB_OT_load_assets(Operator): + bl_idname = "vse_toolbox.load_assets" + bl_label = "Load Assets for current projects" + bl_description = "Load Assets" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + settings = get_scene_settings() + if settings.active_project: + return True + + def execute(self, context): + settings = get_scene_settings() + prefs = get_addon_prefs() + tracker = prefs.tracker + + tracker.connect() + project = settings.active_project + project.assets.clear() + + assets = tracker.get_assets(project['id']) + if not assets: + self.report({'ERROR'}, f'No Assets found for {project.name}.') + + for asset_data in assets: + asset = project.assets.add() + + asset.name = asset_data['id'] + asset.norm_name = norm_name(asset_data['name'], separator=' ', format=str.lower) + + asset.tracker_name = asset_data['name'] + asset.id = asset_data['id'] + asset.asset_type = asset_data['asset_type'] + + #for key, value in asset_data.get('data', {}).items(): + asset['metadata'] = asset_data.get('data', {}) + + preview_id = asset_data.get('preview_file_id') + if preview_id: + asset.preview = preview_id + preview_path = Path(PREVIEWS_DIR / project.id / preview_id).with_suffix('.png') + tracker.download_preview(preview_id, preview_path) + + if preview_id not in ASSET_PREVIEWS: + ASSET_PREVIEWS.load(preview_id, preview_path.as_posix(), 'IMAGE', True) + + + self.report({'INFO'}, f'Assets for {project.name} successfully loaded') + return {"FINISHED"} + + +class VSETB_OT_load_projects(Operator): + bl_idname = "vse_toolbox.load_projects" + bl_label = "Load Projects" + bl_description = "Load Projects" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + settings = get_scene_settings() + prefs = get_addon_prefs() + tracker = prefs.tracker + + old_project_name = settings.project_name.replace(' ', '_').upper() + + settings.projects.clear() + tracker.connect() + + for project_data in tracker.get_projects(): + project = settings.projects.add() + project.type = project_data['production_type'].upper().replace(' ', '') + project.name = project_data['name'] + project.id = project_data['id'] + + if project.type == 'TVSHOW': + for episode_data in tracker.get_episodes(project_data): + episode = project.episodes.add() + episode.name = episode_data['name'] + episode.id = episode_data['id'] + + for metadata_data in tracker.get_metadata_types(project_data): + #pprint(metadata_data) + metadata_type = project.metadata_types.add() + metadata_type.name = metadata_data['name'] + metadata_type.field_name = metadata_data['field_name'] + #metadata_type['choices'] = metadata_data['choices'] + + if prefs.sort_metadata_items: + metadata_data['choices'].sort() + + for choice in metadata_data['choices']: + choice_item = metadata_type.choices.add() + choice_item.name = choice + + metadata_type['entity_type'] = metadata_data['entity_type'].upper() + + for status_data in tracker.get_task_statuses(project_data): + #print(metadata_data) + task_status = project.task_statuses.add() + task_status.name = status_data['short_name'].upper() + + for task_type_data in tracker.get_shot_task_types(project_data): + task_type = project.task_types.add() + task_type.name = task_type_data['name'] + + for asset_type_data in tracker.get_asset_types(project_data): + asset_type = project.asset_types.add() + asset_type.name = asset_type_data['name'] + + project.set_spreadsheet() + + for project in settings.projects: + if project.name.replace(' ', '_').upper() == old_project_name: + settings.project_name = project.name + + bpy.ops.vse_toolbox.load_settings() + #else: + # print('Could Not restore Project Name') + + #if settings.active_project: + # settings.active_project.set_strip_metadata() + + #bpy.ops.vse_toolbox.load_assets() + + self.report({"INFO"}, 'Successfully Load Tracker Projects') + + return {'FINISHED'} + + +class VSETB_OT_new_episode(Operator): + bl_idname = "vse_toolbox.new_episode" + bl_label = "New Epispde" + bl_description = "Add new Episode to Project" + bl_options = {"REGISTER", "UNDO"} + + episode_name : StringProperty(name="Episode Name", default="") + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + scn = context.scene + settings = get_scene_settings() + + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + settings = get_scene_settings() + prefs = get_addon_prefs() + tracker = prefs.tracker + + episode_name = settings.episode_template.format(index=int(self.episode_name)) + + #print(self.episode_name) + #print('episode_name: ', episode_name) + + episode = tracker.get_episode(episode_name) + if episode: + self.report({'ERROR'}, f'Episode {episode_name} already exists') + return {"CANCELLED"} + + tracker.new_episode(episode_name) + # tracker.get_episodes + tracker.update_project() + self.report({'INFO'}, f'Episode {episode_name} successfully created') + return {'FINISHED'} + + +def get_task_status_items(self, context): + settings = get_scene_settings() + project = settings.active_project + + status_items = [('CURRENT', 'Current', '')] + + if project: + status_items += [(t.name, t.name, '') for t in project.task_statuses] + return status_items + +def get_task_type_items(self, context): + settings = get_scene_settings() + project = settings.active_project + + if not project: + return [('NONE', 'None', '')] + + return [(t.name, t.name, '') for t in project.task_types] + +class VSETB_OT_upload_to_tracker(Operator): + bl_idname = "vse_toolbox.upload_to_tracker" + bl_label = "Upload to tracker" + bl_description = "Upload selected strip to tracker" + bl_options = {"REGISTER", "UNDO"} + + task : EnumProperty(items=get_task_type_items) + status : EnumProperty(items=get_task_status_items) + comment : StringProperty() + add_preview : BoolProperty(default=True) + preview_mode : EnumProperty(items=[(m, m.title().replace('_', ' '), '') + for m in ('ONLY_NEW', 'REPLACE', 'ADD')]) + set_main_preview : BoolProperty(default=True) + casting : BoolProperty(default=True) + custom_data : BoolProperty(default=True) + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + prefs = get_addon_prefs() + settings = get_scene_settings() + project = settings.active_project + + tracker = prefs.tracker + tracker.connect() + + #self.bl_label = f"Upload to {settings.tracker_name.title()}" + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + scn = context.scene + settings = get_scene_settings() + + layout = self.layout + col = layout.column() + col.use_property_split = True + col.prop(self, 'task', text='Task') + col.prop(self, 'status', text='Status') + col.prop(self, 'comment', text='Comment') + row = col.row(heading='Add Preview') + row.prop(self, 'add_preview', text='') + row.prop(self, 'preview_mode', text='') + col.separator() + col.prop(self, 'casting', text='Casting') + col.prop(self, 'custom_data', text='Custom Data') + + def execute(self, context): + #self.report({'ERROR'}, f'Export not implemented yet.') + prefs = get_addon_prefs() + settings = get_scene_settings() + project = settings.active_project + episode = None + if settings.active_episode: + episode = settings.active_episode.id + + + tracker = prefs.tracker + + status = self.status + if status == 'CURRENT': + status = None + + for strip in get_strips(channel='Shots', selected_only=True): + sequence_name = get_strip_sequence_name(strip) + shot_name = strip.name + sequence = tracker.get_sequence(sequence_name, episode=episode) + metadata = strip.vsetb_strip_settings.metadata.to_dict() + #print(metadata) + + if not sequence: + self.report({"INFO"}, f'Create sequence {sequence_name} in Kitsu') + sequence = tracker.new_sequence(sequence_name, episode=episode) + + shot = tracker.get_shot(shot_name, sequence=sequence) + if not shot: + self.report({"INFO"}, f'Create shot {shot_name} in Kitsu') + shot = tracker.new_shot(shot_name, sequence=sequence) + + task = tracker.get_task(self.task, entity=shot) + if not task: + task = tracker.new_task(shot, task_type=self.task) + + # print('\n', 'task comment') + # print(task['last_comment']) + + preview = None + if self.add_preview: + preview = Path(get_strip_render_path(strip, project.render_template)) + #print(preview) + if not preview.exists(): + print(f'The preview {preview} not exists') + preview = None + + elif task.get('last_comment') and task['last_comment']['previews']: + if self.preview_mode == 'REPLACE': + tracker.remove_comment(task['last_comment']) + elif self.preview_mode == 'ONLY_NEW': + preview = None + + #print(f'{preview=}') + #print(f'{status=}') + if status or preview: + tracker.new_comment(task, comment=self.comment, status=status, preview=preview, set_main_preview=self.set_main_preview) + + if self.custom_data: + metadata = strip.vsetb_strip_settings.metadata.to_dict() + description = strip.vsetb_strip_settings.description + tracker.update_data(shot, metadata, frames=strip.frame_final_duration, description=description) + + if self.casting: + casting = [{'asset_id': a.id, 'nb_occurences': a.instance} for a in strip.vsetb_strip_settings.casting] + tracker.update_casting(shot, casting) + + + return {"FINISHED"} + + +classes = ( + VSETB_OT_load_assets, + VSETB_OT_load_projects, + VSETB_OT_new_episode, + VSETB_OT_tracker_connect, + VSETB_OT_upload_to_tracker, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..9c3bd74 --- /dev/null +++ b/ui/__init__.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +from vse_toolbox.ui import (panels, preferences, properties) + +modules = ( + panels, + preferences, + properties +) + +if 'bpy' in locals(): + import importlib + + for mod in modules: + importlib.reload(mod) + +import bpy + +def register(): + for mod in modules: + mod.register() + +def unregister(): + for mod in modules: + mod.unregister() \ No newline at end of file diff --git a/panels.py b/ui/panels.py similarity index 100% rename from panels.py rename to ui/panels.py diff --git a/preferences.py b/ui/preferences.py similarity index 100% rename from preferences.py rename to ui/preferences.py diff --git a/properties.py b/ui/properties.py similarity index 98% rename from properties.py rename to ui/properties.py index 7eb01fb..cddd66f 100644 --- a/properties.py +++ b/ui/properties.py @@ -126,13 +126,6 @@ class AssetCasting(PropertyGroup): '_name': self.get('_name') } -class SpreadsheetCell(PropertyGroup): - export_name : StringProperty() - enabled : BoolProperty(default=True) - field_name : StringProperty() - type : EnumProperty(items=[(t, t, "") for t in ('METADATA', 'SHOT', 'ASSET_TYPE')]) - #sort : BoolProperty(default=True) - class AssetType(PropertyGroup): __annotations__ = {} @@ -166,12 +159,20 @@ class Episode(PropertyGroup): return self.get(settings.project_name) +class SpreadsheetCell(PropertyGroup): + export_name : StringProperty() + enabled : BoolProperty(default=True) + field_name : StringProperty() + type : EnumProperty(items=[(t, t, "") for t in ('METADATA', 'SHOT', 'ASSET_TYPE')]) + #sort : BoolProperty(default=True) + + def get_custom_name_items(self, context): settings = get_scene_settings() project = settings.active_project return [(m.field_name, m.name, '') for m in project.metadata_types if m.entity_type=='ASSET'] -class SpreadsheetOptions(PropertyGroup): +class SpreadsheetImport(PropertyGroup): format : EnumProperty(items=[(i, i, '') for i in ('CSV', 'XLSX')]) separator : StringProperty(default='\\n') delimiter : StringProperty(default=';') @@ -182,6 +183,8 @@ class SpreadsheetOptions(PropertyGroup): open_folder : BoolProperty(default=False) show_settings : BoolProperty(default=False) + cells: CollectionProperty(type=SpreadsheetCell) + cell_index : IntProperty(name='Spreadsheet Index', default=0) class Project(PropertyGroup): @@ -222,9 +225,8 @@ class Project(PropertyGroup): task_types : CollectionProperty(type=TaskType) task_statuses : CollectionProperty(type=TaskStatus) - spreadsheet_options : PointerProperty(type=SpreadsheetOptions) - spreadsheet : CollectionProperty(type=SpreadsheetCell) - spreadsheet_index : IntProperty(name='Spreadsheet Index', default=0) + spreadsheet_import: PointerProperty(type=Spreadsheet) + spreadsheet_export: PointerProperty(type=Spreadsheet) type : StringProperty() @@ -432,7 +434,7 @@ classes = ( Metadata, MetadataType, TaskType, - SpreadsheetOptions, + Spreadsheet, Project, VSETB_UL_spreadsheet, VSETB_UL_casting,