pull/5/head
“christopheseux” 2023-05-02 18:38:16 +02:00
parent 7f1e2cc0b2
commit 9f11a18c3a
15 changed files with 1655 additions and 1450 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
*pyc
__pycache__
__pycache__
.vscode

View File

@ -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:

View File

@ -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()
for mod in modules:
mod.unregister()

108
operators/addon.py Normal file
View File

@ -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)

327
operators/casting.py Normal file
View File

@ -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)

184
operators/imports.py Normal file
View File

@ -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)

File diff suppressed because it is too large Load Diff

70
operators/render.py Normal file
View File

@ -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)

216
operators/sequencer.py Normal file
View File

@ -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)

319
operators/spreadsheet.py Normal file
View File

@ -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)

367
operators/tracker.py Normal file
View File

@ -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)

25
ui/__init__.py Normal file
View File

@ -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()

View File

@ -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,