vse_toolbox/operators/casting.py
Joseph HENRY d2d64dc8a8 Blender 5.0 compat + STB XML import + sequence/stamps improvements
- Fix Blender 5.0 API: active_sequence_strip → active_strip, bracket
  property access on AddonPreferences, _RestrictContext during register()
- Fix new_effect() frame_end → length for Blender 5.0
- Fix OTIO adapter kwargs (rate/ignore_timecode_mismatch only for cmx_3600)
- Fix OTIO global_start_time RationalTime → int conversion
- Add Import STB XML operator with movie strip import and XML patching
  for Storyboard Pro transitions missing <alignment>
- Add Create Sequence Strip operator (select shots → create sequence)
- Improve Set Stamps: channel at top, no conflict with existing strips,
  use raw strip names in templates
- TVSHOW episode fixes: sequence loading, episode creation
- Kitsu: new_asset, new_episode, admin_connect fix, gazu version pin
- Fix escape warnings in file_utils regex patterns
- Guard handler registration against duplicates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:09:03 +01:00

423 lines
13 KiB
Python

import json
import bpy
from bpy.types import Context, Event, PropertyGroup, Operator
from bpy.props import (CollectionProperty, EnumProperty, StringProperty)
from vse_toolbox.constants import CASTING_BUFFER, ASSET_ITEMS
from vse_toolbox.sequencer_utils import get_strips
from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings, get_strip_settings)
from time import perf_counter
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_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')
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=lambda s,c : ASSET_ITEMS)
#asset_name : EnumProperty(name='', items=get_scene_settings().active_project.get('asset_items', []))
@classmethod
def poll(cls, context):
active_strip = context.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
settings = get_scene_settings()
project = settings.active_project
asset = project.assets[self.asset_name]
strips = get_strips('Shots', selected_only=True)
for strip in strips:
#active_strip = scn.sequence_editor.active_strip
strip_settings = strip.vsetb_strip_settings
if strip_settings.casting.get(self.asset_name):
self.report({'WARNING'}, f"Asset {asset.label} already casted.")
return {"CANCELLED"}
item = strip_settings.casting.add()
item.name = asset.name
item.id = asset.id
item['_name'] = asset.label
strip_settings.casting.update()
self.report({"INFO"}, f'Item {asset.label} added in {len(strips)} shots')
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.active_strip
if active_strip:
return True
def invoke(self, context, event):
scn = context.scene
settings = get_strip_settings()
asset_id = settings.active_casting.id
strips = get_strips('Shots', selected_only=True)
strips_modified = 0
for strip in strips:
strip_settings = strip.vsetb_strip_settings
for i, asset_casting in enumerate(list(strip_settings.casting)):
if asset_casting.id == asset_id:
strip_settings.casting.remove(i)
if strip_settings.casting_index != 0:
strip_settings.casting_index -= 1
strips_modified += 1
continue
if asset := settings.active_casting.asset:
name = asset.label
else:
name = settings.active_casting.get('_name', '')
self.report({"INFO"}, f'Item {name} removed in {strips_modified} shots')
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.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):
if not context.scene.sequence_editor:
return
active_strip = context.active_strip
strip_settings = get_strip_settings()
if active_strip and strip_settings.casting:
return True
def execute(self, context):
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 (ctrl|shift: Add, alt:Remove)"
bl_options = {"REGISTER", "UNDO"}
mode : EnumProperty(items=[(m, m.title(), '') for m in ('REPLACE', 'ADD', 'REMOVE')])
@classmethod
def poll(cls, context):
return context.active_strip
def invoke(self, context, event):
self.mode = 'REPLACE'
if event.ctrl or event.shift:
self.mode = 'ADD'
elif event.alt:
self.mode = 'REMOVE'
return self.execute(context)
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())
casting_ids = set(c['id'] for c in casting_datas)
for strip in context.selected_strips:
strip_settings = strip.vsetb_strip_settings
if self.mode == 'REPLACE':
strip.vsetb_strip_settings.casting.clear()
for casting_data in casting_datas:
if self.mode == 'REMOVE':
for asset_casting in strip_settings.casting:
index = list(strip_settings.casting).index(asset_casting)
if asset_casting.id in casting_ids:
strip_settings.casting.remove(index)
if strip_settings.casting_index != 0:
strip_settings.casting_index -= 1
else:
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_strips and context.active_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_strip
metadata_value = getattr(active_strip.vsetb_strip_settings.metadata, metadata)
for strip in context.selected_strips:
if strip == context.active_strip:
continue
setattr(strip.vsetb_strip_settings.metadata, metadata, metadata_value)
return {"FINISHED"}
def get_asset_type_items(self, context):
settings = get_scene_settings()
project = settings.active_project
if project and project.asset_types:
return [(t.name, t.name, '') for t in project.asset_types]
return [('Character', 'Character', '')]
class VSETB_OT_casting_create_asset(Operator):
bl_idname = "vse_toolbox.casting_create_asset"
bl_label = "Create & Cast Asset"
bl_description = "Create a new asset in Kitsu and add to casting"
bl_options = {"REGISTER", "UNDO"}
asset_name: StringProperty(name="Name")
asset_type: EnumProperty(name="Type", items=get_asset_type_items)
description: StringProperty(name="Description")
@classmethod
def poll(cls, context):
settings = get_scene_settings()
return settings.active_project
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
prefs = get_addon_prefs()
settings = get_scene_settings()
project = settings.active_project
tracker = prefs.tracker
try:
new_asset_data = tracker.new_asset(
self.asset_name, self.asset_type,
project=project.id, description=self.description
)
except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
# Add to local project assets
asset = project.assets.add()
asset.name = new_asset_data['id']
asset.id = new_asset_data['id']
asset.tracker_name = new_asset_data['name']
asset.asset_type = self.asset_type
# Cast to selected shot strips
strips = get_strips('Shots', selected_only=True)
for strip in strips:
strip_settings = strip.vsetb_strip_settings
cast_item = strip_settings.casting.add()
cast_item.name = new_asset_data['id']
cast_item.id = new_asset_data['id']
cast_item['_name'] = self.asset_name
strip_settings.casting.update()
self.report({'INFO'}, f"Created asset '{self.asset_name}' and cast to {len(strips)} shots")
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,
VSETB_OT_casting_create_asset,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)