Compare commits

..
14 Commits
Author SHA1 Message Date
joseph.henry 4f0111e09a fix missing task comments on tracker upload 2026-09-22 17:35:51 +02:00
joseph.henry 4e29c0e699 refactor and fixes for episode handling gav 2026-09-01 15:44:11 +02:00
joseph.henryandClaude Opus 4.7 01fccc63d9 fix update_data: PUT only changed fields, not the whole entity
PUTting the full entity dict back to data/entities/{id} sends server-managed
fields (id, created_at, updated_at, project_id, entity_type_id, ...) that
newer Kitsu rejects with ParameterException. Build a partial payload with
only name/description/nb_frames/data — and only fetch/merge existing data
when the caller is actually updating custom_data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:11:05 +02:00
joseph.henryandClaude Opus 4.7 6b06224f8c fix get_task: use gazu.task.get_task_by_entity (gazu 1.x)
get_task_by_name was removed in gazu 1.x; the addon pins gazu>=1.0,<2.0
so the call raises AttributeError for artists. get_task_by_entity is the
drop-in replacement with the same (entity, task_type) signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:37:25 +02:00
joseph.henryandClaude Opus 4.6 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
florentin.luce c46d78f2ae handle if task doesnt exist 2025-02-19 14:53:40 +01:00
christophe.seux 06c8c28071 active_sequence_strip poll issue 2025-02-05 14:57:25 +01:00
christophe.seux 671b2eeb4e install_module plugins properly 2025-01-23 20:11:13 +01:00
christophe.seux dad1d92e19 install plugins at import edit 2025-01-23 19:58:51 +01:00
christophe.seux 84a5b93387 fix otio, remove double clips 2025-01-23 19:49:28 +01:00
christophe.seux 17f3e0c71b Avoid choosing same source and destination channel in the scene cut detection 2025-01-10 16:31:57 +01:00
christophe.seux 425c842bfd Refacto of the scene cut detection, make it generic and add another algorythm as an option 2025-01-10 16:24:52 +01:00
florentin.luce de16df05ef get latest media from currrent one pattern 2024-12-19 16:43:50 +01:00
florentin.luce 23093a72e7 update media operator 2024-12-19 16:15:34 +01:00
16 changed files with 1295 additions and 444 deletions
+12 -6
View File
@@ -36,28 +36,34 @@ import bpy
def register(): def register():
bpy.app.handlers.frame_change_post.append(update_text_strips) if update_text_strips not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.render_pre.append(update_text_strips) bpy.app.handlers.frame_change_post.append(update_text_strips)
if update_text_strips not in bpy.app.handlers.render_pre:
bpy.app.handlers.render_pre.append(update_text_strips)
for module in modules: for module in modules:
module.register() module.register()
bpy.app.handlers.frame_change_post.append(set_active_strip) if set_active_strip not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(set_active_strip)
prefs = get_addon_prefs() prefs = get_addon_prefs()
#print('\n\n-------------------', prefs.config_path) #print('\n\n-------------------', prefs.config_path)
def unregister(): def unregister():
bpy.app.handlers.frame_change_post.remove(update_text_strips) if update_text_strips in bpy.app.handlers.frame_change_post:
bpy.app.handlers.render_pre.remove(update_text_strips) bpy.app.handlers.frame_change_post.remove(update_text_strips)
if update_text_strips in bpy.app.handlers.render_pre:
bpy.app.handlers.render_pre.remove(update_text_strips)
try: try:
bpy.utils.previews.remove(ASSET_PREVIEWS) bpy.utils.previews.remove(ASSET_PREVIEWS)
except Exception as e: except Exception as e:
pass pass
bpy.app.handlers.frame_change_post.remove(set_active_strip) if set_active_strip in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(set_active_strip)
for module in reversed(modules): for module in reversed(modules):
module.unregister() module.unregister()
-104
View File
@@ -1,104 +0,0 @@
import os
import re
import subprocess
import bpy
from vse_toolbox import bl_utils
def launch_split(movie_strip, threshold, frame_start=None, frame_end=None):
"""Launch ffmpeg command to detect changing frames from a movie strip.
Args:
movie_strip (bpy.types.Sequence): blender sequence strip to detect changes.
threshold (float): value of the detection factor (from 0 to 1).
frame_start (int, optional): first frame to detect.
Defaults to None.
frame_end (int, optional): last frame to detect.
Defaults to None.
Returns:
str: ffmpeg command log.
"""
path = bl_utils.abspath(movie_strip.filepath)
fps = bpy.context.scene.render.fps
if frame_start is None:
frame_start = 0
if frame_end is None:
frame_end = movie_strip.frame_duration
frame_start = frame_start - movie_strip.frame_start
#frame_start += movie_strip.frame_offset_start
#frame_end -= movie_strip.frame_offset_end
# Launch ffmpeg command to split
ffmpeg_cmd = get_command(str(path), threshold, frame_start, frame_end, fps)
print(ffmpeg_cmd)
process = subprocess.Popen(
ffmpeg_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
return process
def get_command(path, threshold, frame_start, frame_end, fps):
"""Generate the ffmpeg command which detect change from a movie.
Args:
path (_type_): path to detect changes.
threshold (_type_): value of the detection factor (from 0 to 1).
frame_start (_type_): first frame to detect.
frame_end (_type_): last frame to detect.
fps (_type_): framerate of the movie.
Returns:
list: ffmpeg command as list for subprocess module.
"""
start_time = frame_start/fps
end_time = frame_end/fps
return [
'ffmpeg',
'-i',
str(path),
'-vf',
f"trim=start={start_time}:end={end_time}, select='gt(scene, {threshold})',showinfo",
'-f',
'null',
'-'
]
def get_split_time(log, as_frame=True, fps=None):
"""Parse ffmpeg command lines to detect the timecode
Args:
log (str): log to parse.
as_frame (bool, optional): if wanted the timecode as frame number.
Defaults to True.
fps (_type_, optional): framerate of the movie (mandatory if as_frame used).
Defaults to None.
Returns:
_type_: _description_
"""
timecodes = re.findall(r'pts_time:([\d.]+)', log)
if not timecodes:
return
timecode = timecodes[0]
if as_frame:
# convert timecode to frame number
return round(float(timecode) * fps)
return timecode
+1 -2
View File
@@ -49,8 +49,7 @@ def get_scene_settings():
return bpy.context.scene.vsetb_settings return bpy.context.scene.vsetb_settings
def get_strip_settings(): def get_strip_settings():
scn = bpy.context.scene strip = bpy.context.active_strip
strip = bpy.context.active_sequence_strip
if not strip: if not strip:
return return
+2 -2
View File
@@ -54,7 +54,7 @@ def norm_str(string, separator='_', format=str.lower, padding=0):
string = string.replace('_', ' ') string = string.replace('_', ' ')
string = string.replace('-', ' ') string = string.replace('-', ' ')
string = re.sub('[ ]+', ' ', string) string = re.sub('[ ]+', ' ', string)
string = re.sub('[ ]+\/[ ]+', '/', string) string = re.sub(r'[ ]+/[ ]+', '/', string)
string = string.strip() string = string.strip()
if format: if format:
@@ -74,7 +74,7 @@ def norm_name(string, separator='_', format=str.lower, padding=0):
string = string.replace('_', ' ') string = string.replace('_', ' ')
string = string.replace('-', ' ') string = string.replace('-', ' ')
string = re.sub('[ ]+', ' ', string) string = re.sub('[ ]+', ' ', string)
string = re.sub('[ ]+\/[ ]+', '/', string) string = re.sub(r'[ ]+/[ ]+', '/', string)
string = string.strip() string = string.strip()
if format: if format:
+77 -17
View File
@@ -53,7 +53,7 @@ class VSETB_OT_casting_replace(Operator):
item = self.assets.add() item = self.assets.add()
item.name = asset.tracker_name item.name = asset.tracker_name
strip = context.active_sequence_strip strip = context.active_strip
asset_casting_index = strip.vsetb_strip_settings.casting_index asset_casting_index = strip.vsetb_strip_settings.casting_index
active_asset = strip.vsetb_strip_settings.casting[asset_casting_index].asset active_asset = strip.vsetb_strip_settings.casting[asset_casting_index].asset
@@ -87,7 +87,7 @@ class VSETB_OT_casting_add(Operator):
#asset_name : EnumProperty(name='', items=get_scene_settings().active_project.get('asset_items', [])) #asset_name : EnumProperty(name='', items=get_scene_settings().active_project.get('asset_items', []))
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
active_strip = context.scene.sequence_editor.active_strip active_strip = context.active_strip
if active_strip: if active_strip:
return True return True
@@ -135,7 +135,7 @@ class VSETB_OT_casting_remove(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
active_strip = context.scene.sequence_editor.active_strip active_strip = context.active_strip
if active_strip: if active_strip:
return True return True
@@ -190,7 +190,7 @@ class VSETB_OT_casting_move(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
active_strip = context.scene.sequence_editor.active_strip active_strip = context.active_strip
if active_strip: if active_strip:
return True return True
@@ -229,16 +229,15 @@ class VSETB_OT_copy_casting(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
active_strip = context.scene.sequence_editor.active_strip if not context.scene.sequence_editor:
return
active_strip = context.active_strip
strip_settings = get_strip_settings() strip_settings = get_strip_settings()
if active_strip and strip_settings.casting: if active_strip and strip_settings.casting:
return True return True
def execute(self, context): def execute(self, context):
scn = context.scene
active_strip = scn.sequence_editor.active_strip
strip_settings = get_strip_settings() strip_settings = get_strip_settings()
datas = [c.to_dict() for c in strip_settings.casting] datas = [c.to_dict() for c in strip_settings.casting]
@@ -257,9 +256,7 @@ class VSETB_OT_paste_casting(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
active_strip = context.scene.sequence_editor.active_strip return context.active_strip
if active_strip:
return True
def invoke(self, context, event): def invoke(self, context, event):
self.mode = 'REPLACE' self.mode = 'REPLACE'
@@ -282,7 +279,7 @@ class VSETB_OT_paste_casting(Operator):
casting_datas = json.loads(CASTING_BUFFER.read_text()) casting_datas = json.loads(CASTING_BUFFER.read_text())
casting_ids = set(c['id'] for c in casting_datas) casting_ids = set(c['id'] for c in casting_datas)
for strip in context.selected_sequences: for strip in context.selected_strips:
strip_settings = strip.vsetb_strip_settings strip_settings = strip.vsetb_strip_settings
if self.mode == 'REPLACE': if self.mode == 'REPLACE':
@@ -320,7 +317,7 @@ class VSETB_OT_copy_metadata(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.selected_sequences and context.active_sequence_strip return context.selected_strips and context.active_strip
def execute(self, context): def execute(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
@@ -332,11 +329,11 @@ class VSETB_OT_copy_metadata(Operator):
if not metadata: if not metadata:
self.report({'ERROR'}, f'No Metadata named {self.metadata}') self.report({'ERROR'}, f'No Metadata named {self.metadata}')
active_strip = context.active_sequence_strip active_strip = context.active_strip
metadata_value = getattr(active_strip.vsetb_strip_settings.metadata, metadata) metadata_value = getattr(active_strip.vsetb_strip_settings.metadata, metadata)
for strip in context.selected_sequences: for strip in context.selected_strips:
if strip == context.active_sequence_strip: if strip == context.active_strip:
continue continue
setattr(strip.vsetb_strip_settings.metadata, metadata, metadata_value) setattr(strip.vsetb_strip_settings.metadata, metadata, metadata_value)
@@ -344,6 +341,68 @@ class VSETB_OT_copy_metadata(Operator):
return {"FINISHED"} 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 = ( classes = (
VSETB_OT_casting_add, VSETB_OT_casting_add,
VSETB_OT_casting_remove, VSETB_OT_casting_remove,
@@ -351,7 +410,8 @@ classes = (
VSETB_OT_copy_casting, VSETB_OT_copy_casting,
VSETB_OT_paste_casting, VSETB_OT_paste_casting,
VSETB_OT_casting_replace, VSETB_OT_casting_replace,
VSETB_OT_copy_metadata VSETB_OT_copy_metadata,
VSETB_OT_casting_create_asset,
) )
def register(): def register():
+1
View File
@@ -217,6 +217,7 @@ class VSETB_OT_export_edl(Operator):
def execute(self, context): def execute(self, context):
opentimelineio = install_module('opentimelineio') opentimelineio = install_module('opentimelineio')
install_module('opentimelineio', package_name='opentimelineio-plugins')
from opentimelineio.schema import (Clip, Timeline, Track, ExternalReference) from opentimelineio.schema import (Clip, Timeline, Track, ExternalReference)
from opentimelineio.opentime import (RationalTime, TimeRange) from opentimelineio.opentime import (RationalTime, TimeRange)
+209 -16
View File
@@ -15,7 +15,7 @@ from vse_toolbox.constants import (EDITS, EDIT_SUFFIXES, MOVIES, MOVIE_SUFFIXES,
from vse_toolbox.sequencer_utils import (clean_sequencer, import_edit, import_movie, from vse_toolbox.sequencer_utils import (clean_sequencer, import_edit, import_movie,
import_sound, get_strips, get_channel_index, get_empty_channel, scale_clip_to_fit) import_sound, get_strips, get_channel_index, get_empty_channel, scale_clip_to_fit)
from vse_toolbox.bl_utils import (get_scene_settings, get_addon_prefs, get_scene_settings, abspath) from vse_toolbox.bl_utils import (get_scene_settings, get_addon_prefs, abspath)
from vse_toolbox.file_utils import install_module, parse, find_last, expand from vse_toolbox.file_utils import install_module, parse, find_last, expand
@@ -90,6 +90,15 @@ class VSETB_OT_import_files(Operator):
import_edit : BoolProperty(name='', default=True) import_edit : BoolProperty(name='', default=True)
edit: EnumProperty(name='', items=lambda s, c: EDITS) edit: EnumProperty(name='', items=lambda s, c: EDITS)
edit_adapter: EnumProperty(
name='Format',
items=[
('AUTO', "Auto-detect", "Detect format from file extension"),
('cmx_3600', "EDL (CMX 3600)", "Standard Edit Decision List"),
('fcp_xml', "FCP 7 XML", "Final Cut Pro 7 XML (Toon Boom Storyboard Pro)"),
],
default='AUTO'
)
match_by : EnumProperty(name='Match By', items=[('NAME', 'Name', ''), ('INDEX', 'Index', '')]) match_by : EnumProperty(name='Match By', items=[('NAME', 'Name', ''), ('INDEX', 'Index', '')])
import_movie : BoolProperty(name='', default=False) import_movie : BoolProperty(name='', default=False)
@@ -119,6 +128,8 @@ class VSETB_OT_import_files(Operator):
sub.active = self.import_edit sub.active = self.import_edit
sub.prop(self, 'edit') sub.prop(self, 'edit')
row = layout.row(align=True) row = layout.row(align=True)
row.prop(self, 'edit_adapter', text='Format')
row = layout.row(align=True)
row.prop(self, 'match_by', expand=True) row.prop(self, 'match_by', expand=True)
layout.separator() layout.separator()
@@ -144,7 +155,7 @@ class VSETB_OT_import_files(Operator):
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def execute(self, context): def execute(self, context):
sequencer = context.scene.sequence_editor.sequences sequencer = context.scene.sequence_editor.strips
edit_filepath = Path(self.directory, self.edit) edit_filepath = Path(self.directory, self.edit)
if not edit_filepath.exists(): if not edit_filepath.exists():
@@ -168,7 +179,12 @@ class VSETB_OT_import_files(Operator):
if self.import_edit: if self.import_edit:
print(f'[>.] Loading Edit from: {str(edit_filepath)}') print(f'[>.] Loading Edit from: {str(edit_filepath)}')
import_edit(edit_filepath, adapter="cmx_3600", match_by=self.match_by) adapter = self.edit_adapter
if adapter == 'AUTO':
ext = edit_filepath.suffix.lower()
adapter = 'fcp_xml' if ext == '.xml' else 'cmx_3600'
import_edit(edit_filepath, adapter=adapter, match_by=self.match_by)
if self.import_movie: if self.import_movie:
print(f'[>.] Loading Movie from: {str(movie_filepath)}') print(f'[>.] Loading Movie from: {str(movie_filepath)}')
@@ -190,7 +206,7 @@ class VSETB_OT_import_files(Operator):
print(f'[>.] Loading Audio from: {str(movie_filepath)}') print(f'[>.] Loading Audio from: {str(movie_filepath)}')
import_sound(movie_filepath) import_sound(movie_filepath)
context.scene.sequence_editor.sequences.update() context.scene.sequence_editor.strips.update()
return {"FINISHED"} return {"FINISHED"}
@@ -382,6 +398,9 @@ class VSETB_OT_import_shots(Operator):
project = settings.active_project project = settings.active_project
task = tracker.get_task(task_type.id or task_type.name, entity=shot) task = tracker.get_task(task_type.id or task_type.name, entity=shot)
if not task:
print(f'No task {task_type.name} found for {shot["name"]}')
return
last_comment = tracker.get_last_comment_with_preview(task) last_comment = tracker.get_last_comment_with_preview(task)
if not last_comment: if not last_comment:
return return
@@ -439,8 +458,9 @@ class VSETB_OT_import_shots(Operator):
for asset_data in casting_data: for asset_data in casting_data:
item = strip_settings.casting.add() item = strip_settings.casting.add()
item.name = asset_data['asset_name'] item.name = asset_data['asset_name']
item.id = asset_data['asset_id'] item.id = asset_data['asset_id']
item.instance = asset_data.get('nb_occurences', 1)
item['_name'] = asset_data['asset_name'] item['_name'] = asset_data['asset_name']
strip_settings.casting.update() strip_settings.casting.update()
@@ -458,26 +478,39 @@ class VSETB_OT_import_shots(Operator):
task_types = [t for t in project.task_types if t.import_enabled] task_types = [t for t in project.task_types if t.import_enabled]
sequences = [s for s in project.sequences if s.import_enabled] sequences = [s for s in project.sequences if s.import_enabled]
if not sequences:
self.report({'ERROR'}, "No sequences selected. For episodic projects, select an episode first.")
return {'CANCELLED'}
conformed = False conformed = False
scn.sequence_editor_clear() if import_shots.clear:
scn.sequence_editor_create() frame_index = 1
scn.sequence_editor_clear()
scn.sequence_editor_create()
else:
frame_index = scn.frame_end +1
self.set_sequencer_channels([t.name for t in task_types]) self.set_sequencer_channels([t.name for t in task_types])
frame_index = 1
for sequence in sequences: for sequence in sequences:
shots_data = tracker.get_shots(sequence=sequence.id) shots_data = tracker.get_shots(sequence=sequence.id)
sequence_start = frame_index sequence_start = frame_index
for shot_data in shots_data: for shot_data in shots_data:
frames = int(shot_data['nb_frames']) frames = shot_data['nb_frames']
if not frames:
frames = 100
print(f'No nb frames on tracker for {shot_data["name"]}')
frames = int(frames)
frame_end = frame_index + frames frame_end = frame_index + frames
strip = scn.sequence_editor.sequences.new_effect( strip = scn.sequence_editor.strips.new_effect(
name=shot_data['name'], name=shot_data['name'],
type='COLOR', type='COLOR',
channel=get_channel_index('Shots'), channel=get_channel_index('Shots'),
frame_start=frame_index, frame_start=frame_index,
frame_end=frame_index + frames length=frames
) )
strip.blend_alpha = 0 strip.blend_alpha = 0
strip.color = (0.5, 0.5, 0.5) strip.color = (0.5, 0.5, 0.5)
@@ -497,9 +530,13 @@ class VSETB_OT_import_shots(Operator):
print(f'Loading Preview from {preview}') print(f'Loading Preview from {preview}')
channel_index = get_channel_index(f'{task_type.name} Video') channel_index = get_channel_index(f'{task_type.name} Video')
video_clip = import_movie(preview, frame_start=frame_index, frame_end=frame_end) video_clip = import_movie(preview, frame_start=frame_index)
video_clip.channel = channel_index video_clip.channel = channel_index
if strip.frame_final_end != video_clip.frame_final_end: # Conform shot duration to longest media
frames = video_clip.frame_final_duration
strip.frame_final_end = video_clip.frame_final_end
if video_clip.frame_offset_end: if video_clip.frame_offset_end:
video_clip.color_tag = 'COLOR_01' video_clip.color_tag = 'COLOR_01'
@@ -511,20 +548,19 @@ class VSETB_OT_import_shots(Operator):
# Load Audio # Load Audio
channel_index = get_channel_index(f'{task_type.name} Audio') channel_index = get_channel_index(f'{task_type.name} Audio')
audio_clip = import_sound(preview, frame_start=frame_index, audio_clip = import_sound(preview, frame_start=frame_index)
frame_end=frame_end)
audio_clip.channel = channel_index audio_clip.channel = channel_index
if video_clip.frame_offset_end: if video_clip.frame_offset_end:
audio_clip.color_tag = 'COLOR_01' audio_clip.color_tag = 'COLOR_01'
frame_index += frames frame_index += frames
strip = scn.sequence_editor.sequences.new_effect( strip = scn.sequence_editor.strips.new_effect(
name=sequence.name, name=sequence.name,
type='COLOR', type='COLOR',
channel=get_channel_index('Sequences'), channel=get_channel_index('Sequences'),
frame_start=sequence_start, frame_start=sequence_start,
frame_end=frame_index length=frame_index - sequence_start
) )
strip.blend_alpha = 0 strip.blend_alpha = 0
strip.color = (0.25, 0.25, 0.25) strip.color = (0.25, 0.25, 0.25)
@@ -632,6 +668,8 @@ class VSETB_OT_import_shots(Operator):
row = split.row() row = split.row()
row.label(icon="ERROR") row.label(icon="ERROR")
row.label(text='Add at least one Sequence') row.label(text='Add at least one Sequence')
layout.prop(import_shots, 'clear', text='Clear')
def invoke(self, context, event): def invoke(self, context, event):
scn = context.scene scn = context.scene
@@ -656,6 +694,160 @@ class VSETB_OT_import_shots(Operator):
return True return True
class VSETB_OT_import_stb_xml(Operator):
bl_idname = "vse_toolbox.import_stb_xml"
bl_label = "Import STB XML"
bl_description = "Import Toon Boom Storyboard Pro FCP XML export with movie strips"
bl_options = {"REGISTER", "UNDO"}
filepath: StringProperty(
name="File Path",
description="Path to the Storyboard Pro FCP XML export",
subtype='FILE_PATH',
)
filter_glob: StringProperty(default="*.xml", options={'HIDDEN'})
import_movies: BoolProperty(
name="Import Movies",
description="Import matching .mov files from the same directory",
default=True,
)
clean_sequencer: BoolProperty(
name="Clean Sequencer",
description="Remove all existing strips before import",
default=True,
)
conform_resolution: BoolProperty(
name="Conform Resolution",
description="Set scene resolution and FPS from the XML metadata",
default=True,
)
@classmethod
def poll(cls, context):
return get_scene_settings().active_project
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def _parse_xml_metadata(self, filepath):
"""Extract resolution and FPS from the FCP XML."""
import xml.etree.ElementTree as ET
tree = ET.parse(filepath)
root = tree.getroot()
meta = {}
fmt = root.find('.//sequence/media/video/format/samplecharacteristics')
if fmt is not None:
w = fmt.find('width')
h = fmt.find('height')
if w is not None and h is not None:
meta['width'] = int(w.text)
meta['height'] = int(h.text)
rate = fmt.find('rate/timebase')
if rate is not None:
meta['fps'] = int(rate.text)
return meta
def execute(self, context):
scn = context.scene
xml_path = Path(self.filepath)
if not xml_path.exists():
self.report({'ERROR'}, f"File not found: {xml_path}")
return {'CANCELLED'}
xml_dir = xml_path.parent
# Conform scene resolution/fps from XML metadata
if self.conform_resolution:
meta = self._parse_xml_metadata(str(xml_path))
if 'width' in meta:
scn.render.resolution_x = meta['width']
scn.render.resolution_y = meta['height']
print(f'[STB] Set resolution to {meta["width"]}x{meta["height"]}')
if 'fps' in meta:
scn.render.fps = meta['fps']
print(f'[STB] Set FPS to {meta["fps"]}')
# Clean sequencer if requested
if self.clean_sequencer:
scn.sequence_editor_clear()
scn.sequence_editor_create()
if not scn.sequence_editor:
scn.sequence_editor_create()
# Set up channels
channels = scn.sequence_editor.channels
channel_names = {'Shots': 1, 'STB Video': 2}
for name, idx in channel_names.items():
if idx < len(channels):
channels[idx].name = name
# Patch XML for OTIO compatibility (STB Pro exports transitions without <alignment>)
import xml.etree.ElementTree as ET
tree = ET.parse(str(xml_path))
patched = False
for trans in tree.iter('transitionitem'):
if trans.find('alignment') is None:
align = ET.SubElement(trans, 'alignment')
align.text = 'center'
patched = True
if patched:
patched_path = Path(bpy.app.tempdir) / xml_path.name
tree.write(str(patched_path), xml_declaration=True, encoding='UTF-8')
import_xml = str(patched_path)
print(f'[STB] Patched {xml_path.name} (added missing <alignment> to transitions)')
else:
import_xml = str(xml_path)
# Import edit (COLOR strips on Shots channel)
print(f'[STB] Importing edit from: {xml_path}')
import_edit(import_xml, adapter='fcp_xml', channel='Shots')
# Import matching movie files
if self.import_movies:
shot_strips = get_strips(channel='Shots')
stb_channel = get_channel_index('STB Video')
for strip in shot_strips:
# Try to find a matching .mov file by strip name
mov_path = xml_dir / f"{strip.name}.mov"
if not mov_path.exists():
# Try source_name (set by import_edit)
source = strip.vsetb_strip_settings.source_name
if source:
mov_path = xml_dir / f"{Path(source).stem}.mov"
if mov_path.exists():
movie_strip = scn.sequence_editor.strips.new_movie(
name=strip.name,
filepath=str(mov_path),
channel=stb_channel,
frame_start=strip.frame_final_start,
)
movie_strip.frame_final_end = strip.frame_final_end
# Scale to fit scene resolution
scale_clip_to_fit(movie_strip)
print(f'[STB] Imported movie: {mov_path.name}')
else:
print(f'[STB] No movie found for strip: {strip.name}')
scn.frame_start = 0
scn.frame_end = max(
(s.frame_final_end for s in scn.sequence_editor.strips),
default=scn.frame_end
)
self.report({'INFO'}, f"Imported {len(get_strips(channel='Shots'))} shots from STB XML")
return {"FINISHED"}
classes = ( classes = (
VSETB_OT_select_sequence, VSETB_OT_select_sequence,
VSETB_OT_unselect_sequence, VSETB_OT_unselect_sequence,
@@ -666,6 +858,7 @@ classes = (
VSETB_UL_import_task, VSETB_UL_import_task,
VSETB_OT_auto_select_files, VSETB_OT_auto_select_files,
VSETB_OT_import_files, VSETB_OT_import_files,
VSETB_OT_import_stb_xml,
VSETB_OT_import_shots, VSETB_OT_import_shots,
) )
+338 -83
View File
@@ -1,5 +1,7 @@
import re
from os.path import expandvars, abspath from os.path import expandvars, abspath
from pathlib import Path from pathlib import Path
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator
from bpy.props import (BoolProperty, StringProperty, FloatProperty, from bpy.props import (BoolProperty, StringProperty, FloatProperty,
@@ -7,10 +9,10 @@ from bpy.props import (BoolProperty, StringProperty, FloatProperty,
from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels, from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels,
get_channel_index, new_text_strip, get_strip_at, get_channel_name, get_channel_index, new_text_strip, get_strip_at, get_channel_name,
create_shot_strip) create_shot_strip, update_text_strips)
from vse_toolbox import auto_splitter from vse_toolbox.scene_cut_detection import detect_scene_change
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings, get_addon_prefs
from shutil import copy2 from shutil import copy2
@@ -22,7 +24,11 @@ class VSETB_OT_rename(Operator):
#template : StringProperty(name="Strip Name", default="") #template : StringProperty(name="Strip Name", default="")
#increment : IntProperty(name="Increment", default=0) #increment : IntProperty(name="Increment", default=0)
selected_only : BoolProperty(name="Selected Only", default=True) selected_only : BoolProperty(
name="Selected Only",
default=False,
description="Rename only selected strips; disabled renames the whole channel",
)
#start_number : IntProperty(name="Start Number", default=0, min=0) #start_number : IntProperty(name="Start Number", default=0, min=0)
#by_sequence : BoolProperty( #by_sequence : BoolProperty(
# name="Reset By Sequence", # name="Reset By Sequence",
@@ -32,9 +38,13 @@ class VSETB_OT_rename(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
settings = get_scene_settings() strip = context.active_strip
strip = context.active_sequence_strip if strip is None and context.scene.sequence_editor:
return settings.active_project and get_channel_name(strip) in ('Shots', 'Sequences') strip = next(
(s for s in context.scene.sequence_editor.strips if s.select),
None,
)
return strip is not None
def invoke(self, context, event): def invoke(self, context, event):
scn = context.scene scn = context.scene
@@ -47,19 +57,29 @@ class VSETB_OT_rename(Operator):
scn = context.scene scn = context.scene
settings = get_scene_settings() settings = get_scene_settings()
project = settings.active_project project = settings.active_project
if project is None and settings.projects:
project = settings.projects[0]
if project is None:
self.report({'ERROR'}, 'No VSE Toolbox project is loaded')
return
episode = project.episode_name episode = project.episode_name
sequence = str(project.sequence_start_number).zfill(project.sequence_padding) sequence = str(project.sequence_start_number).zfill(project.sequence_padding)
shot = str(project.shot_start_number).zfill(project.shot_padding) shot = str(project.shot_start_number).zfill(project.shot_padding)
strip = context.active_sequence_strip strip = context.active_strip
if strip is None and context.scene.sequence_editor:
strip = next(
(s for s in context.scene.sequence_editor.strips if s.select),
None,
)
channel_name = get_channel_name(strip) channel_name = get_channel_name(strip)
col = layout.column() col = layout.column()
col.use_property_split = True col.use_property_split = True
col.use_property_decorate = False col.use_property_decorate = False
if channel_name == 'Shots': if channel_name != 'Sequences':
col.prop(project, 'shot_template', text='Shot Name') col.prop(project, 'shot_template', text='Shot Name')
col.prop(project, 'shot_start_number', text='Start Number') col.prop(project, 'shot_start_number', text='Start Number')
col.prop(project, 'shot_increment', text='Increment') col.prop(project, 'shot_increment', text='Increment')
@@ -75,9 +95,9 @@ class VSETB_OT_rename(Operator):
col.prop(self, 'selected_only') col.prop(self, 'selected_only')
if channel_name == 'Shots': if channel_name != 'Sequences':
label = project.shot_template.format(episode=episode, sequence=sequence, shot=shot) label = project.shot_template.format(episode=episode, sequence=sequence, shot=shot)
elif channel_name == 'Sequences': else:
label = project.sequence_template.format(episode=episode, sequence=sequence) label = project.sequence_template.format(episode=episode, sequence=sequence)
col.label(text=f'Renaming {label}') col.label(text=f'Renaming {label}')
@@ -86,12 +106,30 @@ class VSETB_OT_rename(Operator):
scn = context.scene scn = context.scene
settings = get_scene_settings() settings = get_scene_settings()
project = settings.active_project project = settings.active_project
if project is None:
# The config path is set during addon registration, but the
# project data is only populated when Load Settings is pressed.
bpy.ops.vse_toolbox.load_settings()
project = settings.active_project
if project is None and settings.projects:
project = settings.projects[0]
if project is None:
self.report({'ERROR'}, 'No VSE Toolbox project is loaded')
return {'CANCELLED'}
# Keep the scene's active project in sync with the fallback project;
# rename_strips reads it again from scene settings.
settings.project_name = project.name
strip = context.active_sequence_strip strip = context.active_strip
if strip is None and context.scene.sequence_editor:
strip = next(
(s for s in context.scene.sequence_editor.strips if s.select),
None,
)
channel_name = get_channel_name(strip) channel_name = get_channel_name(strip)
strips = get_strips(channel=channel_name, selected_only=self.selected_only) strips = get_strips(channel=channel_name, selected_only=self.selected_only)
if channel_name == 'Shots': if channel_name != 'Sequences':
rename_strips(strips, rename_strips(strips,
template=project.shot_template, template=project.shot_template,
increment=project.shot_increment, start_number=project.shot_start_number, increment=project.shot_increment, start_number=project.shot_start_number,
@@ -150,8 +188,13 @@ class VSETB_OT_set_sequencer(Operator):
movie = movies[0] movie = movies[0]
movie.transform.scale_x = movie.transform.scale_y = 1 movie.transform.scale_x = movie.transform.scale_y = 1
elem = movie.strip_elem_from_frame(scn.frame_current) elem = movie.strip_elem_from_frame(scn.frame_current)
scn.render.resolution_x = elem.orig_width if elem is None and movie.elements:
scn.render.resolution_y = elem.orig_height elem = movie.elements[0]
if elem is not None:
scn.render.resolution_x = elem.orig_width
scn.render.resolution_y = elem.orig_height
else:
self.report({'WARNING'}, 'Cannot determine movie resolution.')
else: else:
self.report({'INFO'}, f'Cannot set Resolution. No Movie Found.') self.report({'INFO'}, f'Cannot set Resolution. No Movie Found.')
@@ -174,30 +217,63 @@ class VSETB_OT_set_sequencer(Operator):
return {"FINISHED"} return {"FINISHED"}
class VSETB_OT_auto_split(Operator): class VSETB_OT_scene_cut_detection(Operator):
"""Launch subprocess with ffmpeg and python to find and create each """Launch subprocess with ffmpeg and python to find and create each
shots strips from video source""" shots strips from video source"""
bl_idname = "vse_toolbox.auto_split" bl_idname = "vse_toolbox.scene_cut_detection"
bl_label = "Auto Split" bl_label = "Scene Cut Detection"
bl_description = "Generate shots strips" bl_description = "Detect scene change and create strips on the destination channel, use crop to restric detection"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
threshold: FloatProperty(name="Threshold", default=0.6, min=0, max=1) animated_threshold: FloatProperty(name="Threshold", default=0.5, min=0, max=1,
frame_first: IntProperty(name='Start Split') description='Probability for the current frame to introduce a new scene')
frame_last: IntProperty(name='End Split') still_threshold: FloatProperty(name="Threshold", default=0.001, min=0, max=1, precision=4,
movie_channel_name: EnumProperty( description="Noise tolerance, difference ratio between 0 and 1")
frame_start: IntProperty(name='Start Split')
frame_end: IntProperty(name='End Split')
source_channel_name: EnumProperty(
items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels), items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels),
name='Movie Channel') name='Source Channel')
destination_channel_name: EnumProperty(
items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels),
name='Destination Channel')
movie_type: EnumProperty(
items=[('ANIMATED', 'Animated', 'Use select filter from ffmpeg, best for animated frame'),
('STILL', 'Still', 'Use freezedetect filter from ffmpeg, best for board or text')],
name='Movie Type', default='ANIMATED')
def invoke(self, context, event): def invoke(self, context, event):
self.frame_first = context.scene.frame_start sequencer = context.scene.sequence_editor
self.frame_last = context.scene.frame_end channels = sequencer.channels if sequencer else None
selected_strip = context.active_strip
if selected_strip is None and context.selected_strips:
selected_strip = context.selected_strips[0]
if context.selected_sequences: # Prefer the channel containing the selected strip. With no selected
self.frame_first = min([s.frame_final_start for s in context.selected_sequences]) # strip, use the first available channel (channel 0 in the UI list).
self.frame_last = max([s.frame_final_end for s in context.selected_sequences]) if selected_strip is not None:
source_channel = selected_strip.channel
self.source_channel_name = get_channel_name(selected_strip)
elif channels and len(channels):
source_channel = 0
self.source_channel_name = channels[0].name
else:
source_channel = 0
# Put the result on the immediately following channel when it exists.
# Do not overwrite it when that channel is unavailable.
if channels and source_channel + 1 < len(channels):
destination_channel = channels[source_channel + 1]
self.destination_channel_name = destination_channel.name
self.frame_start = context.scene.frame_start
self.frame_end = context.scene.frame_end
if context.selected_strips:
self.frame_start = min([s.frame_final_start for s in context.selected_strips])
self.frame_end = max([s.frame_final_end for s in context.selected_strips])
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
@@ -208,54 +284,72 @@ class VSETB_OT_auto_split(Operator):
col.use_property_split = True col.use_property_split = True
col.use_property_decorate = False col.use_property_decorate = False
col.prop(self, 'threshold') row = col.row(align=True)
col.prop(self, 'movie_channel_name') row.prop(self, 'movie_type', expand=True)
if self.movie_type == 'ANIMATED':
col.prop(self, 'animated_threshold')
else:
col.prop(self, 'still_threshold')
col.prop(self, 'source_channel_name')
col.prop(self, 'destination_channel_name')
split_col = col.column(align=True) split_col = col.column(align=True)
split_col.prop(self, 'frame_first', text='Frame Split First') split_col.prop(self, 'frame_start', text='Frame Split First')
split_col.prop(self, 'frame_last', text='Last') split_col.prop(self, 'frame_end', text='Last')
def execute(self, context): def execute(self, context):
if self.source_channel_name == self.destination_channel_name:
self.report({"ERROR"}, 'Source and Destination cannot be the same channel')
return {'CANCELLED'}
context.window_manager.modal_handler_add(self) context.window_manager.modal_handler_add(self)
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
def modal(self, context, event): def modal(self, context, event):
scn = context.scene
# Replace the previous detection result instead of mixing it with the
# new one. This is especially important when changing thresholds.
for old_strip in list(scn.sequence_editor.strips):
if old_strip.name.startswith("tmp_shot_"):
scn.sequence_editor.strips.remove(old_strip)
strips = get_strips(channel=self.movie_channel_name) strips = get_strips(channel=self.source_channel_name)
i = 1 i = 1
frame_start = self.frame_first frame_start = self.frame_start
for strip in strips: for strip in strips:
if strip.type != 'MOVIE': if strip.type != 'MOVIE':
continue continue
# Skip strip outside the frame range to create shot from. # Skip strip outside the frame range to create shot from.
if strip.frame_final_start >= self.frame_last or strip.frame_final_end <= self.frame_first: if strip.frame_final_start >= self.frame_end or strip.frame_final_end <= self.frame_start:
continue continue
threshold = self.animated_threshold if self.movie_type == 'ANIMATED' else self.still_threshold
process = auto_splitter.launch_split(strip, self.threshold, frame_start=self.frame_first, frame_end=self.frame_last) params = dict(strip=strip, movie_type= self.movie_type, threshold=threshold,
frame_start=self.frame_start, frame_end=self.frame_end,
for line in process.stdout: crop=(strip.crop.min_x, strip.crop.max_x, strip.crop.min_y, strip.crop.max_y))
# Get frame split from the movie timeline (not from blender strips timeline)
frame_end = auto_splitter.get_split_time(line, fps=24)
for frame_end in detect_scene_change(**params):
if not frame_end: if not frame_end:
continue continue
# Convert movie frame to strips frame # Convert movie frame to strips frame
if frame_start+int(strip.frame_start) < self.frame_first: if frame_start+int(strip.frame_start) < self.frame_start:
frame_start = self.frame_first frame_start = self.frame_start
frame_end += int(strip.frame_final_start) frame_end += int(strip.frame_final_start)
if frame_end > self.frame_last: if frame_end > self.frame_end:
frame_end = self.frame_last frame_end = self.frame_end
create_shot_strip( create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}', f'tmp_shot_{str(i).zfill(3)}',
start=frame_start, start=frame_start,
end=frame_end end=frame_end,
channel=self.destination_channel_name
) )
i += 1 i += 1
@@ -263,14 +357,15 @@ class VSETB_OT_auto_split(Operator):
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
process.wait() #process.wait()
# Last strip: # Last strip:
if frame_start < self.frame_last: if frame_start < self.frame_end:
create_shot_strip( create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}', f'tmp_shot_{str(i).zfill(3)}',
start=frame_start, start=frame_start,
end=self.frame_last end=self.frame_end,
channel=self.destination_channel_name
) )
return {'FINISHED'} return {'FINISHED'}
@@ -286,12 +381,22 @@ class VSETB_OT_set_stamps(Operator):
scn = context.scene scn = context.scene
settings = get_scene_settings() settings = get_scene_settings()
project = settings.active_project project = settings.active_project
#strip_settings = get_strip_settings()
channel_index = get_channel_index('Stamps')
# Remove existing stamps
for strip in get_strips('Stamps'): for strip in get_strips('Stamps'):
if strip.type == 'META': if strip.type == 'META':
scn.sequence_editor.sequences.remove(strip) scn.sequence_editor.strips.remove(strip)
# Ensure a Stamps channel exists at the top
stamps_channel = get_channel_index('Stamps')
if stamps_channel == 0:
# Find the highest used channel and place Stamps above it
all_strips = list(scn.sequence_editor.strips)
max_channel = max((s.channel for s in all_strips), default=0)
stamps_channel = max_channel + 1
channels = scn.sequence_editor.channels
if stamps_channel < len(channels):
channels[stamps_channel].name = 'Stamps'
bpy.ops.sequencer.select_all(action='DESELECT') bpy.ops.sequencer.select_all(action='DESELECT')
@@ -304,7 +409,9 @@ class VSETB_OT_set_stamps(Operator):
crop_x = int(width * 0.4) crop_x = int(width * 0.4)
crop_max_y = int(height - font_size*2) crop_max_y = int(height - font_size*2)
#crop_min_y = int(scn.render.resolution_y * 0.01)
# Use temporary high channels for text strips before meta grouping
tmp_base = stamps_channel + 1
stamp_params = dict(start=scn.frame_start, end=scn.frame_end, stamp_params = dict(start=scn.frame_start, end=scn.frame_end,
font_size=font_size, y=margin, box_margin=box_margin, select=True, box_color=(0, 0, 0, 0.5)) font_size=font_size, y=margin, box_margin=box_margin, select=True, box_color=(0, 0, 0, 0.5))
@@ -312,37 +419,38 @@ class VSETB_OT_set_stamps(Operator):
# Project Name # Project Name
project_text = '{project}' project_text = '{project}'
if project.type == 'TVSHOW': if project.type == 'TVSHOW':
project_text = '{project} / ep{episode}' project_text = '{project} / ep {episode}'
project_strip_stamp = new_text_strip('project_stamp', channel=1, **stamp_params, project_strip_stamp = new_text_strip('project_stamp', channel=tmp_base, **stamp_params,
text=project_text, x=0.01, align_x='LEFT', align_y='BOTTOM') text=project_text, x=0.01, anchor_x='LEFT', anchor_y='BOTTOM')
project_strip_stamp.crop.max_x = crop_x * 2 project_strip_stamp.crop.max_x = crop_x * 2
project_strip_stamp.crop.max_y = crop_max_y project_strip_stamp.crop.max_y = crop_max_y
# Shot Name # Shot Name
shot_strip_stamp = new_text_strip('shot_stamp', channel=tmp_base + 1, **stamp_params,
shot_strip_stamp = new_text_strip('shot_stamp', channel=2, **stamp_params, text='{sequence_strip} / {strip}', anchor_y='BOTTOM')
text='sq{sequence} / sh{shot}', align_y='BOTTOM')
shot_strip_stamp.crop.min_x = crop_x shot_strip_stamp.crop.min_x = crop_x
shot_strip_stamp.crop.max_x = crop_x shot_strip_stamp.crop.max_x = crop_x
shot_strip_stamp.crop.max_y = crop_max_y shot_strip_stamp.crop.max_y = crop_max_y
# Frame # Frame
frame_strip_stamp = new_text_strip('frame_stamp', channel=3, **stamp_params, frame_strip_stamp = new_text_strip('frame_stamp', channel=tmp_base + 2, **stamp_params,
text='{shot_frame} / {shot_duration} {timecode}', x=0.99, align_x='RIGHT', align_y='BOTTOM') text='{shot_frame} / {shot_duration} {timecode}', x=0.99, anchor_x='RIGHT', anchor_y='BOTTOM')
frame_strip_stamp.crop.min_x = crop_x *2 frame_strip_stamp.crop.min_x = crop_x *2
frame_strip_stamp.crop.max_y = crop_max_y frame_strip_stamp.crop.max_y = crop_max_y
bpy.ops.sequencer.meta_make() bpy.ops.sequencer.meta_make()
stamps_strip = context.active_sequence_strip stamps_strip = context.active_strip
stamps_strip.name = 'Stamps' stamps_strip.name = 'Stamps'
stamps_strip.channel = channel_index stamps_strip.channel = stamps_channel
#stamps_strip = scn.sequence_editor.sequences.new_meta('Stamps', scn.frame_start, scn.frame_end) #stamps_strip = scn.sequence_editor.sequences.new_meta('Stamps', scn.frame_start, scn.frame_end)
#stamps_strip.channel = get_channel_index('Stamps') #stamps_strip.channel = get_channel_index('Stamps')
scn.frame_set(scn.frame_current) # For update stamps # frame_set() does not trigger frame_change_post when the frame does
# not change, so update the stamp text explicitly for the first view.
update_text_strips(scn)
return {"FINISHED"} return {"FINISHED"}
@@ -365,7 +473,7 @@ class VSETB_OT_previous_shot(Operator):
active_strip_index = strips.index(active_strip) active_strip_index = strips.index(active_strip)
next_shot = strips[active_strip_index - 1] next_shot = strips[active_strip_index - 1]
context.scene.frame_set(next_shot.frame_final_start) context.scene.frame_set(next_shot.frame_final_start)
bpy.ops.sequencer.select_all(action="DESELECT") bpy.ops.sequencer.select_all(action="DESELECT")
next_shot.select = True next_shot.select = True
context.scene.sequence_editor.active_strip = next_shot context.scene.sequence_editor.active_strip = next_shot
@@ -392,7 +500,7 @@ class VSETB_OT_next_shot(Operator):
active_strip_index = strips.index(active_strip) active_strip_index = strips.index(active_strip)
next_shot = strips[active_strip_index + 1] next_shot = strips[active_strip_index + 1]
context.scene.frame_set(next_shot.frame_final_start) context.scene.frame_set(next_shot.frame_final_start)
bpy.ops.sequencer.select_all(action="DESELECT") bpy.ops.sequencer.select_all(action="DESELECT")
next_shot.select = True next_shot.select = True
context.scene.sequence_editor.active_strip = next_shot context.scene.sequence_editor.active_strip = next_shot
@@ -408,7 +516,7 @@ class VSETB_OT_open_strip_folder(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
strip = context.active_sequence_strip strip = context.active_strip
if not strip: if not strip:
cls.poll_message_set('No active') cls.poll_message_set('No active')
return return
@@ -426,7 +534,7 @@ class VSETB_OT_open_strip_folder(Operator):
'Sequences': 'sequence_dir' 'Sequences': 'sequence_dir'
} }
strip = context.active_sequence_strip strip = context.active_strip
settings = get_scene_settings() settings = get_scene_settings()
project = settings.active_project project = settings.active_project
@@ -469,12 +577,12 @@ class VSETB_OT_collect_files(Operator):
cls.poll_message_set('Save the blend to collect files') cls.poll_message_set('Save the blend to collect files')
def execute(self, context): def execute(self, context):
strip = context.active_sequence_strip strip = context.active_strip
settings = get_scene_settings() settings = get_scene_settings()
project = settings.active_project project = settings.active_project
strips = [s for s in context.scene.sequence_editor.sequences_all if s.type in ('MOVIE', 'SOUND')] strips = [s for s in context.scene.sequence_editor.strips_all if s.type in ('MOVIE', 'SOUND')]
context.window_manager.progress_begin(0, len(strips)) context.window_manager.progress_begin(0, len(strips))
for i, strip in enumerate(strips): for i, strip in enumerate(strips):
@@ -524,18 +632,18 @@ class VSETB_OT_insert_channel(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_sequence_strip return context.active_strip
def execute(self, context): def execute(self, context):
scn = context.scene scn = context.scene
channel_index = context.active_sequence_strip.channel channel_index = context.active_strip.channel
strips = list(scn.sequence_editor.sequences) strips = list(scn.sequence_editor.strips)
for strip in sorted(strips, key=lambda x: x.channel, reverse=True): for strip in sorted(strips, key=lambda x: x.channel, reverse=True):
if strip.channel >= channel_index: if strip.channel >= channel_index:
strip.channel += 1 strip.channel += 1
channels = {i: (c.name, c.lock, c.mute) for i, c in enumerate(scn.sequence_editor.channels)} channels = {i: (c.name, c.lock, c.mute) for i, c in enumerate(scn.sequence_editor.channels)}
for i in sorted(channels.keys()): for i in sorted(channels.keys()):
channel = scn.sequence_editor.channels[i] channel = scn.sequence_editor.channels[i]
@@ -567,16 +675,16 @@ class VSETB_OT_remove_channel(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_sequence_strip return context.active_strip
def execute(self, context): def execute(self, context):
scn = context.scene scn = context.scene
channel_index = context.active_sequence_strip.channel channel_index = context.active_strip.channel
if [s for s in scn.sequence_editor.sequences if s.channel == channel_index-1]: if [s for s in scn.sequence_editor.strips if s.channel == channel_index-1]:
self.report({"WARNING"}, "Channel Bellow not empty") self.report({"WARNING"}, "Channel Bellow not empty")
strips = list(scn.sequence_editor.sequences) strips = list(scn.sequence_editor.strips)
for strip in sorted(strips, key=lambda x: x.channel): for strip in sorted(strips, key=lambda x: x.channel):
if strip.channel >= channel_index: if strip.channel >= channel_index:
@@ -677,7 +785,7 @@ class VSETB_OT_merge_shot_strips(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
selected_strips = bpy.context.selected_sequences selected_strips = bpy.context.selected_strips
if len(selected_strips) <= 1: if len(selected_strips) <= 1:
return False return False
@@ -685,16 +793,161 @@ class VSETB_OT_merge_shot_strips(Operator):
def execute(self, context): def execute(self, context):
selected_strips = bpy.context.selected_sequences selected_strips = bpy.context.selected_strips
last_frame = selected_strips[-1].frame_final_end last_frame = selected_strips[-1].frame_final_end
for i in range(1, len(selected_strips)): for i in range(1, len(selected_strips)):
context.scene.sequence_editor.sequences.remove(selected_strips[i]) context.scene.sequence_editor.strips.remove(selected_strips[i])
selected_strips[0].frame_final_end = last_frame selected_strips[0].frame_final_end = last_frame
return {"FINISHED"} return {"FINISHED"}
class VSETB_OT_create_sequence_strip(Operator):
"""Create a sequence strip spanning the selected shot strips."""
bl_idname = "vse_toolbox.create_sequence_strip"
bl_label = "Create Sequence Strip"
bl_options = {"REGISTER", "UNDO"}
sequence_name: StringProperty(
name="Sequence Name",
description="Name of the sequence (e.g. SC010)",
default="SQ010",
)
@classmethod
def poll(cls, context):
selected = context.selected_strips
if not selected:
cls.poll_message_set('No strips selected')
return False
if not any(get_channel_name(s) == 'Shots' for s in selected):
cls.poll_message_set('Select strips on the Shots channel')
return False
return True
def invoke(self, context, event):
selected_shots = sorted(
(s for s in context.selected_strips if get_channel_name(s) == 'Shots'),
key=lambda s: s.frame_final_start,
)
if selected_shots:
first_frame = selected_shots[0].frame_final_start
sequence_channel = get_channel_index('Sequences')
previous = None
if sequence_channel:
previous = max(
(
s for s in context.scene.sequence_editor.strips
if s.channel == sequence_channel
and s.frame_final_end <= first_frame
),
key=lambda s: s.frame_final_end,
default=None,
)
if previous:
match = re.search(r'(\d+)$', previous.name)
if match:
settings = get_scene_settings()
project = settings.active_project
increment = project.sequence_increment if project else 10
number = int(match.group(1)) + increment
self.sequence_name = (
previous.name[:match.start()]
+ str(number).zfill(len(match.group(1)))
)
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
shot_strips = sorted(
[s for s in context.selected_strips if get_channel_name(s) == 'Shots'],
key=lambda s: s.frame_final_start,
)
if not shot_strips:
self.report({'ERROR'}, "No shot strips selected")
return {'CANCELLED'}
scn = context.scene
seq_channel = get_channel_index('Sequences')
if seq_channel == 0:
# Insert a Sequences channel below the Shots channel
shots_channel = get_channel_index('Shots')
if not shots_channel:
self.report({'ERROR'}, "No 'Shots' channel found")
return {'CANCELLED'}
seq_channel = shots_channel
# Push all strips at or above this channel up by one
all_strips = list(scn.sequence_editor.strips)
for s in sorted(all_strips, key=lambda x: x.channel, reverse=True):
if s.channel >= seq_channel:
s.channel += 1
# Shift channel names up
channels = scn.sequence_editor.channels
chan_data = {i: (c.name, c.lock, c.mute) for i, c in enumerate(channels)}
for i in sorted(chan_data.keys(), reverse=True):
if i >= seq_channel:
prev = chan_data[i]
ch = channels[i + 1] if (i + 1) < len(channels) else None
if ch:
ch.name, ch.lock, ch.mute = prev
channels[seq_channel].name = 'Sequences'
channels[seq_channel].lock = False
channels[seq_channel].mute = False
frame_start = shot_strips[0].frame_final_start
frame_end = shot_strips[-1].frame_final_end
strip = scn.sequence_editor.strips.new_effect(
name=self.sequence_name,
type='COLOR',
channel=seq_channel,
frame_start=frame_start,
length=frame_end - frame_start,
)
strip.blend_alpha = 0
strip.color_tag = 'COLOR_07' # Purple
self.report({'INFO'}, f"Created sequence '{self.sequence_name}' ({len(shot_strips)} shots)")
return {"FINISHED"}
class VSETB_OT_update_media(Operator):
bl_idname = "vse_toolbox.update_media"
bl_label = "Update Media"
bl_description = "Update selected source"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not (strip := context.active_strip) or strip.type not in ('MOVIE', 'SOUND'):
cls.poll_message_set('No active AUDIO or MOVIE strips')
return
prefs = get_addon_prefs()
if prefs.tracker:
return True
def execute(self, context):
for strip in context.selected_strips:
current_movie = Path(abspath(bpy.path.abspath(strip.filepath)))
pattern_name = re.sub(r'[^\s._\?]+', '*', current_movie.name)
latest_file = sorted(list(current_movie.parent.glob(pattern_name)))[-1]
if latest_file != current_movie:
print(latest_file)
strip.filepath = str(latest_file)
return {'FINISHED'}
addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
@@ -729,7 +982,7 @@ def unregister_keymaps():
classes = ( classes = (
VSETB_OT_rename, VSETB_OT_rename,
VSETB_OT_set_sequencer, VSETB_OT_set_sequencer,
VSETB_OT_auto_split, VSETB_OT_scene_cut_detection,
VSETB_OT_set_stamps, VSETB_OT_set_stamps,
VSETB_OT_show_waveform, VSETB_OT_show_waveform,
VSETB_OT_previous_shot, VSETB_OT_previous_shot,
@@ -739,6 +992,8 @@ classes = (
VSETB_OT_insert_channel, VSETB_OT_insert_channel,
VSETB_OT_remove_channel, VSETB_OT_remove_channel,
VSETB_OT_merge_shot_strips, VSETB_OT_merge_shot_strips,
VSETB_OT_create_sequence_strip,
VSETB_OT_update_media,
WM_OT_split_view, WM_OT_split_view,
) )
+9 -5
View File
@@ -341,7 +341,7 @@ class VSETB_OT_import_spreadsheet(Operator):
settings = get_scene_settings() settings = get_scene_settings()
project = settings.active_project project = settings.active_project
spreadsheet = project.spreadsheet_import spreadsheet = project.spreadsheet_import
sequencer = scn.sequence_editor.sequences sequencer = scn.sequence_editor.strips
assets_missing = set() assets_missing = set()
@@ -355,7 +355,9 @@ class VSETB_OT_import_spreadsheet(Operator):
#print(SPREADSHEET[:2]) #print(SPREADSHEET[:2])
cell_types = project.get_cell_types() cell_types = project.get_cell_types()
cell_names = {k: spreadsheet.cells[k].import_name for k in header if k}
header_enabled = [x for x in header if spreadsheet.cells[x].enabled]
cell_names = {x: spreadsheet.cells[x].import_name for x in header_enabled if x}
#separator = spreadsheet.separator.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r') #separator = spreadsheet.separator.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
@@ -364,10 +366,12 @@ class VSETB_OT_import_spreadsheet(Operator):
frame_start = scn.frame_start frame_start = scn.frame_start
for row in SPREADSHEET[1:]: for row in SPREADSHEET[1:]:
#print(row) #print(row)
cell_data = {cell_names[k]: v for k, v in zip(header, row) if k} cell_data = {cell_names[k]: v for k, v in zip(header, row) if k in cell_names}
#cell_data = {k: v for k, v in zip(header, row)}
shot_name = cell_data['Shot'] shot_name = cell_data['Shot']
if not shot_name:
raise Exception()
#strip = next((s for s in sequencer if s.vsetb_strip_settings.source_name == shot_name), None) #strip = next((s for s in sequencer if s.vsetb_strip_settings.source_name == shot_name), None)
strip = next((s for s in shot_strips if s.name == shot_name), None) strip = next((s for s in shot_strips if s.name == shot_name), None)
@@ -393,7 +397,7 @@ class VSETB_OT_import_spreadsheet(Operator):
type='COLOR', type='COLOR',
channel=channel, channel=channel,
frame_start=frame_start, frame_start=frame_start,
frame_end=frame_end, length=frame_end - frame_start,
) )
strip.blend_alpha = 0.0 strip.blend_alpha = 0.0
+95 -20
View File
@@ -148,18 +148,27 @@ class VSETB_OT_load_projects(Operator):
episode_datas = tracker.get_episodes(project_data) episode_datas = tracker.get_episodes(project_data)
for episode_data in episode_datas: for episode_data in episode_datas:
episode = project.episodes.get(episode_data['name']) episode = project.episodes.get(episode_data['name'])
if not episode: if not episode:
episode = project.episodes.add() episode = project.episodes.add()
episode.name = episode_data['name'] episode.name = episode_data['name']
episode.id = episode_data['id'] episode.id = episode_data['id']
# Clear deleted episodes # Clear deleted episodes
ep_names = [e['name'] for e in episode_datas] ep_names = [e['name'] for e in episode_datas]
for ep in reversed(project.episodes): for ep in reversed(project.episodes):
if ep.name not in ep_names: if ep.name not in ep_names:
project.episodes.remove(list(project.episodes).index(ep)) project.episodes.remove(list(project.episodes).index(ep))
# Load sequences for first episode so they're available immediately
if project.episodes:
first_episode = project.episodes[0]
project.sequences.clear()
for seq in tracker.get_sequences(project_data, episode=first_episode.id):
sequence = project.sequences.add()
sequence.name = seq['name']
sequence.id = seq['id']
else: else:
# Add sequences # Add sequences
sequences_data = tracker.get_sequences(project_data) sequences_data = tracker.get_sequences(project_data)
@@ -196,6 +205,7 @@ class VSETB_OT_load_projects(Operator):
#print(metadata_data) #print(metadata_data)
task_status = project.task_statuses.add() task_status = project.task_statuses.add()
task_status.name = status_data['short_name'].upper() task_status.name = status_data['short_name'].upper()
task_status.is_done = status_data.get('is_done', False)
project.task_types.clear() project.task_types.clear()
for task_type_data in tracker.get_shot_task_types(project_data): for task_type_data in tracker.get_shot_task_types(project_data):
@@ -219,7 +229,10 @@ class VSETB_OT_load_projects(Operator):
if project.name not in project_names: if project.name not in project_names:
settings.projects.remove(list(settings.projects).index(project)) settings.projects.remove(list(settings.projects).index(project))
if self.ctrl or not settings.get('projects_loaded'): # ``load_projects`` can also be called programmatically (for example
# from the upload dialog), bypassing ``invoke`` and therefore not
# initializing ``self.ctrl``.
if getattr(self, 'ctrl', False) or not settings.get('projects_loaded'):
bpy.ops.vse_toolbox.load_settings() bpy.ops.vse_toolbox.load_settings()
if prev_project_name != '/' and prev_project_name in settings.projects: if prev_project_name != '/' and prev_project_name in settings.projects:
@@ -257,15 +270,24 @@ class VSETB_OT_new_episode(Operator):
settings = get_scene_settings() settings = get_scene_settings()
prefs = get_addon_prefs() prefs = get_addon_prefs()
tracker = prefs.tracker tracker = prefs.tracker
project = settings.active_project
episode_name = settings.episode_template.format(index=int(self.episode_name)) if not project:
episode = tracker.get_episode(episode_name) self.report({'ERROR'}, 'No active project')
if episode: return {"CANCELLED"}
episode_name = project.episode_template.format(index=int(self.episode_name))
episode_data = tracker.get_episode(episode_name, project=project.id)
if episode_data:
self.report({'ERROR'}, f'Episode {episode_name} already exists') self.report({'ERROR'}, f'Episode {episode_name} already exists')
return {"CANCELLED"} return {"CANCELLED"}
tracker.new_episode(episode_name) episode_data = tracker.new_episode(episode_name, project=project.id)
tracker.update_project()
# Add to local episode collection
episode = project.episodes.add()
episode.name = episode_data['name']
episode.id = episode_data['id']
self.report({'INFO'}, f'Episode {episode_name} successfully created') self.report({'INFO'}, f'Episode {episode_name} successfully created')
@@ -275,11 +297,19 @@ class VSETB_OT_new_episode(Operator):
class VSETB_OT_upload_to_tracker(Operator): class VSETB_OT_upload_to_tracker(Operator):
bl_idname = "vse_toolbox.upload_to_tracker" bl_idname = "vse_toolbox.upload_to_tracker"
bl_label = "Upload to tracker" bl_label = "Upload to tracker"
bl_description = "Upload selected strip to tracker" bl_description = (
"Upload the selected strips on the Shots track to the tracker. "
"Creates missing sequences, shots and tasks, posts a comment "
'(with a preview, rendered if "Render Strips" is enabled) and '
"updates frames, metadata, casting and task comments."
)
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not get_strips(channel='Shots', selected_only=True):
cls.poll_message_set('Select at least one shot strip')
return False
return True return True
def invoke(self, context, event): def invoke(self, context, event):
@@ -289,7 +319,14 @@ class VSETB_OT_upload_to_tracker(Operator):
tracker = prefs.tracker tracker = prefs.tracker
tracker.connect() tracker.connect()
# Loading the toolbox config can create the selected project locally,
# but does not populate its tracker metadata. In that case the task
# enum has no items even though Kitsu has shot task types configured.
if self.project and not self.project.task_types:
bpy.ops.vse_toolbox.load_projects()
self.project = settings.active_project
#self.bl_label = f"Upload to {settings.tracker_name.title()}" #self.bl_label = f"Upload to {settings.tracker_name.title()}"
return context.window_manager.invoke_props_dialog(self, width=350) return context.window_manager.invoke_props_dialog(self, width=350)
@@ -345,16 +382,35 @@ class VSETB_OT_upload_to_tracker(Operator):
format_data = {**settings.format_data, **self.project.format_data} format_data = {**settings.format_data, **self.project.format_data}
status = upload_to_tracker.status status = upload_to_tracker.status
if status == 'CURRENT': keep_current_status = status == 'CURRENT'
if keep_current_status:
status = None status = None
# ``CURRENT`` means preserve an existing task's status. A newly
# created task has no current status, so use Kitsu's done metadata for
# the first upload (prefer DONE when several statuses are marked done).
default_new_task_status = None
if keep_current_status:
done_statuses = [s for s in self.project.task_statuses if s.is_done]
default_new_task_status = next(
(s.name for s in done_statuses if s.name.lower() == 'done'),
done_statuses[0].name if done_statuses else None,
)
shot_strips = get_strips(channel='Shots', selected_only=True) shot_strips = get_strips(channel='Shots', selected_only=True)
if not shot_strips:
self.report({'WARNING'}, 'Select at least one shot strip to upload')
return {'CANCELLED'}
context.window_manager.progress_begin(0, len(shot_strips)) context.window_manager.progress_begin(0, len(shot_strips))
for i, strip in enumerate(shot_strips): for i, strip in enumerate(shot_strips):
context.window_manager.progress_update(i) context.window_manager.progress_update(i)
strip_settings = strip.vsetb_strip_settings strip_settings = strip.vsetb_strip_settings
sequence_name = get_strip_sequence_name(strip) sequence_name = get_strip_sequence_name(strip)
if sequence_name == 'NoSequence':
self.report({'WARNING'}, f"Strip '{strip.name}' has no sequence — skipped")
continue
shot_name = strip.name shot_name = strip.name
sequence = tracker.get_sequence(sequence_name, episode=episode) sequence = tracker.get_sequence(sequence_name, episode=episode)
metadata = strip_settings.metadata.to_dict(use_name=False) metadata = strip_settings.metadata.to_dict(use_name=False)
@@ -371,7 +427,11 @@ class VSETB_OT_upload_to_tracker(Operator):
task = tracker.get_task(upload_to_tracker.task, entity=shot) task = tracker.get_task(upload_to_tracker.task, entity=shot)
if not task: if not task:
task = tracker.new_task(shot, task_type=upload_to_tracker.task) task = tracker.new_task(
shot,
task_type=upload_to_tracker.task,
status=default_new_task_status or status,
)
preview = None preview = None
if upload_to_tracker.add_preview: if upload_to_tracker.add_preview:
@@ -401,6 +461,7 @@ class VSETB_OT_upload_to_tracker(Operator):
preview = None preview = None
comment_data = None comment_data = None
preview_data = None
if status or upload_to_tracker.comment or preview: if status or upload_to_tracker.comment or preview:
comment_data = tracker.new_comment(task, comment=upload_to_tracker.comment, status=status) comment_data = tracker.new_comment(task, comment=upload_to_tracker.comment, status=status)
if preview: if preview:
@@ -410,16 +471,23 @@ class VSETB_OT_upload_to_tracker(Operator):
comment=comment_data, comment=comment_data,
preview=preview) preview=preview)
if upload_to_tracker.set_main_preview: if upload_to_tracker.set_main_preview and preview_data:
bpy.app.timers.register(partial(tracker.set_main_preview, preview_data), first_interval=10) bpy.app.timers.register(partial(tracker.set_main_preview, preview_data), first_interval=10)
params = {} params = {}
if upload_to_tracker.custom_data: if upload_to_tracker.custom_data:
params['custom_data'] = metadata params['custom_data'] = metadata
params['description'] = strip_settings.description params['description'] = strip_settings.description
if upload_to_tracker.update_frames: if upload_to_tracker.update_frames:
# Blender's frame_final_end is exclusive, while Kitsu's
# frame_out is inclusive. Keep both the explicit range and
# the duration in sync with the uploaded strip.
params['frames'] = strip.frame_final_duration params['frames'] = strip.frame_final_duration
params.setdefault('custom_data', {}).update({
'frame_in': strip.frame_final_start,
'frame_out': strip.frame_final_end - 1,
})
if params: if params:
tracker.update_data(shot, **params) tracker.update_data(shot, **params)
@@ -431,11 +499,18 @@ class VSETB_OT_upload_to_tracker(Operator):
if upload_to_tracker.tasks_comment: if upload_to_tracker.tasks_comment:
for task_type in self.project.task_types: for task_type in self.project.task_types:
task = getattr(strip_settings.tasks, norm_name(task_type.name)) task_settings = getattr(strip_settings.tasks, norm_name(task_type.name))
tracker_task = tracker.get_task(task_type.name, entity=shot) if not task_settings.comment:
continue
if task.comment and tracker_task.get('last_comment') != task.comment: tracker_task = tracker.get_task(task_type.name, entity=shot)
tracker.new_comment(tracker_task, comment=task.comment) if not tracker_task:
tracker_task = tracker.new_task(shot, task_type=task_type.name)
last_comment = tracker_task.get('last_comment')
last_comment_text = last_comment.get('text', '') if isinstance(last_comment, dict) else str(last_comment or '')
if last_comment_text != task_settings.comment:
tracker.new_comment(tracker_task, comment=task_settings.comment)
context.window_manager.progress_end() context.window_manager.progress_end()
+47 -16
View File
@@ -15,7 +15,7 @@ from vse_toolbox.file_utils import install_module, norm_str
from vse_toolbox.resources.trackers.tracker import Tracker from vse_toolbox.resources.trackers.tracker import Tracker
try: try:
gazu = install_module('gazu') gazu = install_module('gazu', package_name='gazu>=1.0,<2.0')
except Exception as e: except Exception as e:
print('Could not install gazu') print('Could not install gazu')
print(e) print(e)
@@ -32,10 +32,15 @@ class Kitsu(Tracker):
admin_password : StringProperty(subtype='PASSWORD') admin_password : StringProperty(subtype='PASSWORD')
def admin_connect(self): def admin_connect(self):
url = self.url url = expandvars(self.url)
if not url:
raise ConnectionError("No Kitsu URL configured")
if not url.endswith('/api'): if not url.endswith('/api'):
url += '/api' url += '/api'
gazu.client.set_host(url)
login = expandvars(self.admin_login or self.login) login = expandvars(self.admin_login or self.login)
password = expandvars(self.admin_password or self.password) password = expandvars(self.admin_password or self.password)
@@ -115,6 +120,11 @@ class Kitsu(Tracker):
return os.environ['TRACKER_PROJECT_NAME'] return os.environ['TRACKER_PROJECT_NAME']
def get_projects(self): def get_projects(self):
# gazu caches this endpoint indefinitely by default. This matters for
# projects created or configured after Blender started: the project
# may be present locally (from the toolbox config) while its task
# types are never loaded from Kitsu.
gazu.cache.clear_all()
return gazu.project.all_open_projects() return gazu.project.all_open_projects()
def get_episode(self, episode, project=None): def get_episode(self, episode, project=None):
@@ -272,7 +282,7 @@ class Kitsu(Tracker):
entity = self.get_id(entity) entity = self.get_id(entity)
task_type = self.get_task_type(task) task_type = self.get_task_type(task)
task = gazu.task.get_task_by_name(entity, task_type) task = gazu.task.get_task_by_entity(entity, task_type)
if not task: if not task:
return return
@@ -347,6 +357,26 @@ class Kitsu(Tracker):
status = self.get_task_status(status) status = self.get_task_status(status)
return gazu.task.new_task(entity, task_type=task_type, task_status=status) return gazu.task.new_task(entity, task_type=task_type, task_status=status)
def new_asset(self, name, asset_type_name, project=None, description="", episode=None):
project = self.get_project(project)
project_dict = {'id': self.get_id(project)}
asset_type = gazu.asset.get_asset_type_by_name(asset_type_name)
if not asset_type:
raise ValueError(f"Asset type '{asset_type_name}' not found in Kitsu")
episode_dict = {'id': self.get_id(episode)} if episode else None
return gazu.asset.new_asset(
project_dict, asset_type, name,
description=description, episode=episode_dict
)
def new_episode(self, name, project=None):
project = self.get_project(project)
return gazu.shot.new_episode({'id': self.get_id(project)}, name)
def update_project(self, project=None):
"""Refresh project data — no-op for now, caller should reload via load_projects."""
pass
def new_sequence(self, sequence, episode=None, project=None): def new_sequence(self, sequence, episode=None, project=None):
project = self.get_project(project) project = self.get_project(project)
@@ -390,27 +420,28 @@ class Kitsu(Tracker):
def update_data(self, entity, custom_data={}, name=None, description=None, frames=None, clear=False): def update_data(self, entity, custom_data={}, name=None, description=None, frames=None, clear=False):
if isinstance(entity, dict): if isinstance(entity, dict):
entity_id = entity['id'] entity_id = entity['id']
existing_data = entity.get('data') or {}
else: else:
entity_id = self.get_id(entity) entity_id = self.get_id(entity)
entity = gazu.client.fetch_one('entities', entity_id) existing_data = None
payload = {}
if name: if name:
entity['name'] = name payload['name'] = name
if description: if description:
entity['description'] = description payload['description'] = description
if frames: if frames:
entity['nb_frames'] = frames payload['nb_frames'] = frames
if clear or not entity['data']: if custom_data or clear:
entity['data'] = custom_data if clear:
else: payload['data'] = dict(custom_data)
entity['data'].update(custom_data) else:
if existing_data is None:
existing_data = gazu.client.fetch_one('entities', entity_id).get('data') or {}
payload['data'] = {**existing_data, **custom_data}
#print('######UPDATE DATA') entity_data = gazu.client.put(f"data/entities/{entity_id}", payload)
#pprint(entity)
entity_data = gazu.client.put(f"data/entities/{entity_id}", entity)
#print()
#pprint(entity)
return entity_data['data'] return entity_data['data']
+148
View File
@@ -0,0 +1,148 @@
import os
import re
import subprocess
import bpy
from vse_toolbox import bl_utils
def detect_scene_change(strip, movie_type="ANIMATED", threshold=0.5, frame_start=None, frame_end=None, crop=None):
"""Launch ffmpeg command to detect changing frames from a movie strip.
Args:
movie_strip (bpy.types.Strip): blender sequence strip to detect changes.
threshold (float): value of the detection factor (from 0 to 1).
frame_start (int, optional): first frame to detect.
Defaults to None.
frame_end (int, optional): last frame to detect.
Defaults to None.
Returns:
str: ffmpeg command log.
"""
path = bl_utils.abspath(strip.filepath)
fps = bpy.context.scene.render.fps
if frame_start is None:
frame_start = 0
if frame_end is None:
frame_end = strip.frame_duration
frame_start = frame_start - strip.frame_start
start_time = frame_start/fps
end_time = frame_end/fps
#frame_start += strip.frame_offset_start
#frame_end -= strip.frame_offset_end
if movie_type == 'ANIMATED':
return select_generator(path, start_time=start_time, end_time=end_time, threshold=threshold, crop=crop)
elif movie_type == 'STILL':
return freeze_detect_generator(path, start_time=start_time, end_time=end_time, threshold=threshold, crop=crop)
else:
raise Exception(f'movie_type: {movie_type} not implemented')
def freeze_detect_generator(path, start_time, end_time, threshold=0.005, crop=None):
"""Generate the ffmpeg command which detect change from a movie.
Args:
path (_type_): path to detect changes.
threshold (_type_): value of the detection factor (from 0 to 1).
frame_start (_type_): first frame to detect.
frame_end (_type_): last frame to detect.
fps (_type_): framerate of the movie.
Returns:
list: ffmpeg command as list for subprocess module.
"""
if crop is None:
crop = [0, 0, 0, 0]
crop_expr = f"crop=iw-{crop[0]}-{crop[1]}:ih-{crop[2]}-{crop[3]}:{crop[0]}:{crop[-1]}"
command = [
'ffmpeg',
'-nostats',
'-i',
str(path),
'-ss', str(start_time),
'-t', str(end_time),
'-vf',
f"{crop_expr}, freezedetect=n={threshold}:d=0.01,metadata=print", #,
'-f',
'null',
'-'
]
print(command)
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
def parse_split_time(log):
print(log)
timecodes = re.findall(r'freeze_end: ([\d.]+)', log)
if timecodes:
return round(float(timecodes[0]) * bpy.context.scene.render.fps)
return filter(None, (parse_split_time(x) for x in process.stdout))
def select_generator(path, start_time, end_time, threshold, crop=None):
"""Generate the ffmpeg command which detect change from a movie.
Args:
path (_type_): path to detect changes.
threshold (_type_): value of the detection factor (from 0 to 1).
frame_start (_type_): first frame to detect.
frame_end (_type_): last frame to detect.
fps (_type_): framerate of the movie.
Returns:
list: ffmpeg command as list for subprocess module.
"""
if crop is None:
crop = [0, 0, 0, 0]
crop_expr = f"crop=iw-{crop[0]}-{crop[1]}:ih-{crop[2]}-{crop[3]}:{crop[0]}:{crop[-1]}"
command = [
'ffmpeg',
'-nostats',
'-i',
str(path),
'-ss', str(start_time),
'-t', str(end_time),
'-vf',
f"{crop_expr}, select='gt(scene, {threshold})', showinfo",
'-f',
'null',
'-'
]
print(command)
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
def parse_split_time(log):
print(log)
timecodes = re.findall(r'pts_time:([\d.]+)', log)
if timecodes:
return round(float(timecodes[0]) * bpy.context.scene.render.fps)
return filter(None, (parse_split_time(x) for x in process.stdout))
+82 -37
View File
@@ -41,17 +41,17 @@ def frame_to_timecode(frame, fps):
return timecode return timecode
def new_text_strip(name='Text', channel=0, start=0, end=50, text='Text', font_size=48, def new_text_strip(name='Text', channel=0, start=0, end=50, text='Text', font_size=48,
x=0.5, y=0.5, align_x='CENTER', align_y='CENTER', select=False, x=0.5, y=0.5, anchor_x='CENTER', anchor_y='CENTER', select=False,
box_color=None, box_margin=0.005): box_color=None, box_margin=0.005):
sequences = bpy.context.scene.sequence_editor.sequences strips = bpy.context.scene.sequence_editor.strips
strip = sequences.new_effect(name, 'TEXT', channel, frame_start=start, frame_end=end) strip = strips.new_effect(name, 'TEXT', channel, frame_start=start, length=end - start)
strip.select = select strip.select = select
strip.text = text strip.text = text
strip.location.x = x strip.location.x = x
strip.location.y = y strip.location.y = y
strip.align_y = align_y strip.anchor_y = anchor_y
strip.align_x = align_x strip.anchor_x = anchor_x
strip.channel = channel strip.channel = channel
strip.font_size = font_size strip.font_size = font_size
@@ -74,7 +74,7 @@ def get_strips(channel=0, selected_only=False):
if isinstance(channel, str): if isinstance(channel, str):
channel = get_channel_index(channel) channel = get_channel_index(channel)
strips = [s for s in scn.sequence_editor.sequences if s.channel==channel] strips = [s for s in scn.sequence_editor.strips if s.channel==channel]
if selected_only: if selected_only:
strips = [s for s in strips if s.select] strips = [s for s in strips if s.select]
@@ -103,11 +103,27 @@ def get_channel_name(strip):
return scn.sequence_editor.channels[strip.channel].name return scn.sequence_editor.channels[strip.channel].name
def get_strip_sequence_name(strip): def get_strip_sequence_name(strip):
sequence_strip = get_strip_at(channel='Sequences', frame=strip.frame_final_start) """Return the sequence strip directly above a shot strip."""
scn = bpy.context.scene
sequencer = scn.sequence_editor
if not sequencer or not strip:
return 'NoSequence'
# In the GAV layout, sequence strips are placed immediately above shot
# strips (for example, Sequences on channel 3 and Shots on channel 2).
sequence_strip = next(
(
candidate for candidate in sequencer.strips
if candidate.channel == strip.channel + 1
and candidate.frame_final_start <= strip.frame_final_start
and candidate.frame_final_end >= strip.frame_final_end
),
None,
)
if sequence_strip: if sequence_strip:
return sequence_strip.name return sequence_strip.name
else:
return 'NoSequence' return 'NoSequence'
def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_sequence=False): def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_sequence=False):
scn = bpy.context.scene scn = bpy.context.scene
@@ -121,12 +137,24 @@ def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_
prev_sequence_name = None prev_sequence_name = None
strip_number = 0 strip_number = 0
for strip in strips: planned_names = []
for strip in strips:
channel = get_channel_name(strip) channel = get_channel_name(strip)
sequence_name = get_strip_sequence_name(strip) sequence_name = get_strip_sequence_name(strip)
format_data = {} format_data = {}
if channel == 'Shots': if channel == 'Shots':
format_data = parse(project.sequence_template, sequence_name) format_data = (
parse(project.sequence_template, sequence_name)
or parse(project.sequence_template, sequence_name.lower())
or {}
)
if 'sequence' not in format_data:
# Shots created directly from an animatic do not have a
# parent Sequences strip yet. Use the configured first
# sequence number until one is created.
format_data['sequence'] = str(
project.sequence_start_number
).zfill(project.sequence_padding)
else: else:
format_data['sequence'] = str(strip_number*increment + start_number).zfill(padding) format_data['sequence'] = str(strip_number*increment + start_number).zfill(padding)
@@ -141,15 +169,18 @@ def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_
name = template.format(**format_data) name = template.format(**format_data)
existing_strip = scn.sequence_editor.sequences_all.get(name) planned_names.append((strip, name))
if existing_strip: prev_sequence_name = sequence_name
existing_strip.name = f"{name}_tmp" strip_number += 1
# Clear all names first so renaming never collides with a later strip and
# leaves a permanent `_tmp` suffix behind.
for index, (strip, _) in enumerate(planned_names):
strip.name = f"__vse_rename_{index:04d}"
for strip, name in planned_names:
print(f'Renaming {strip.name} -> {name}') print(f'Renaming {strip.name} -> {name}')
strip.name = name strip.name = name
prev_sequence_name = sequence_name
strip_number += 1
def set_channels(): def set_channels():
scn = bpy.context.scene scn = bpy.context.scene
@@ -326,11 +357,12 @@ def render_strip(strip, output, attributes=None):
def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'): def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
opentimelineio = install_module('opentimelineio') opentimelineio = install_module('opentimelineio')
install_module('opentimelineio', package_name='opentimelineio-plugins')
from opentimelineio.schema import Clip from opentimelineio.schema import Clip
scn = bpy.context.scene scn = bpy.context.scene
sequencer = scn.sequence_editor.sequences sequencer = scn.sequence_editor.strips
strips = get_strips(channel='Shots') strips = get_strips(channel='Shots')
shot_channel = get_channel_index('Shots') shot_channel = get_channel_index('Shots')
@@ -340,23 +372,31 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
s.channel = empty_channel s.channel = empty_channel
edl = Path(filepath) edl = Path(filepath)
# Extra kwargs only supported by cmx_3600 adapter
extra_kwargs = {}
if adapter == 'cmx_3600':
extra_kwargs = dict(rate=scn.render.fps, ignore_timecode_mismatch=True)
try: try:
timeline = opentimelineio.adapters.read_from_file( timeline = opentimelineio.adapters.read_from_file(
str(edl), adapter, rate=scn.render.fps, ignore_timecode_mismatch=True) str(edl), adapter, **extra_kwargs)
except: except:
print("[>.] read_from_file Failed. Using read_from_string method.") print("[>.] read_from_file Failed. Using read_from_string method.")
data = edl.read_text(encoding='latin-1') data = edl.read_text(encoding='latin-1')
timeline = opentimelineio.adapters.read_from_string( timeline = opentimelineio.adapters.read_from_string(
data, adapter, rate=scn.render.fps, ignore_timecode_mismatch=True) data, adapter, **extra_kwargs)
scn.frame_start = ( global_start = timeline.global_start_time
0 if timeline.global_start_time is None else timeline.global_start_time if global_start is None:
) scn.frame_start = 0
else:
scn.frame_start = int(opentimelineio.opentime.to_frames(global_start))
# Get all video clips only # Get all video clips only
clips = [] clips = []
for track in timeline.tracks: for track in timeline.tracks:
for child in track.each_child(shallow_search=True): for child in track.find_clips(shallow_search=True):
# FIXME Exclude Gaps for now. Gaps are Transitions, Blank Spaces... # FIXME Exclude Gaps for now. Gaps are Transitions, Blank Spaces...
if not isinstance(child, Clip): if not isinstance(child, Clip):
@@ -366,12 +406,16 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
if any(child.name.lower().endswith(ext) for ext in SOUND_SUFFIXES): if any(child.name.lower().endswith(ext) for ext in SOUND_SUFFIXES):
continue continue
if [c for c in clips if child.range_in_parent() == c.range_in_parent()]:
continue
clips.append(child) clips.append(child)
clips.sort(key=lambda x: x.range_in_parent().start_time) clips.sort(key=lambda x: x.range_in_parent().start_time)
for i, clip in enumerate(clips): for i, clip in enumerate(clips):
#print(clip)
frame_start = opentimelineio.opentime.to_frames( frame_start = opentimelineio.opentime.to_frames(
clip.range_in_parent().start_time) clip.range_in_parent().start_time)
@@ -390,6 +434,7 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
print(f'No strip found for {clip.name}') print(f'No strip found for {clip.name}')
if strip: if strip:
#print('Strip already existing', clip)
if frame_start != strip.frame_final_start or frame_end !=strip.frame_final_end: if frame_start != strip.frame_final_start or frame_end !=strip.frame_final_end:
print(f'The strip {strip.name} is updated with new range') print(f'The strip {strip.name} is updated with new range')
#self.report({'INFO'}, f'The strip {strip.name} is updated with new range') #self.report({'INFO'}, f'The strip {strip.name} is updated with new range')
@@ -399,13 +444,13 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
strip.frame_final_end = frame_end strip.frame_final_end = frame_end
else: else:
print('Create a new strip') print('Create a new strip', clip)
strip = sequencer.new_effect( strip = sequencer.new_effect(
name=clip.name, name=clip.name,
type='COLOR', type='COLOR',
channel=shot_channel, channel=shot_channel,
frame_start=frame_start, frame_start=frame_start,
frame_end=frame_end, length=frame_end - frame_start,
) )
strip.blend_alpha = 0.0 strip.blend_alpha = 0.0
@@ -433,7 +478,7 @@ def import_movie(filepath, frame_start=None, frame_end=None):
if len(relpath.as_posix()) < len(filepath.as_posix()): if len(relpath.as_posix()) < len(filepath.as_posix()):
filepath = relpath filepath = relpath
strip = scn.sequence_editor.sequences.new_movie( strip = scn.sequence_editor.strips.new_movie(
name=Path(filepath).stem, name=Path(filepath).stem,
filepath=str(filepath), filepath=str(filepath),
channel=get_channel_index('Movie'), channel=get_channel_index('Movie'),
@@ -474,7 +519,7 @@ def import_sound(filepath, frame_start=None, frame_end=None):
if len(relpath.as_posix()) < len(filepath.as_posix()): if len(relpath.as_posix()) < len(filepath.as_posix()):
filepath = relpath filepath = relpath
strip = scn.sequence_editor.sequences.new_sound( strip = scn.sequence_editor.strips.new_sound(
name=f'{filepath.stem} Audio', name=f'{filepath.stem} Audio',
filepath=str(filepath), filepath=str(filepath),
channel=get_channel_index('Audio'), channel=get_channel_index('Audio'),
@@ -489,7 +534,7 @@ def import_sound(filepath, frame_start=None, frame_end=None):
return strip return strip
def get_empty_channel(): def get_empty_channel():
return max(s.channel for s in bpy.context.scene.sequence_editor.sequences) + 1 return max(s.channel for s in bpy.context.scene.sequence_editor.strips) + 1
def clean_sequencer(edit=False, movie=False, sound=False): def clean_sequencer(edit=False, movie=False, sound=False):
scn = bpy.context.scene scn = bpy.context.scene
@@ -503,7 +548,7 @@ def clean_sequencer(edit=False, movie=False, sound=False):
sequences.extend(get_strips('Audio')) sequences.extend(get_strips('Audio'))
for sequence in sequences: for sequence in sequences:
scn.sequence_editor.sequences.remove(sequence) scn.sequence_editor.strips.remove(sequence)
@persistent @persistent
def set_active_strip(scene): def set_active_strip(scene):
@@ -525,7 +570,7 @@ def set_active_strip(scene):
@persistent @persistent
def update_text_strips(scene): def update_text_strips(scene):
if not scene.sequence_editor or not scene.sequence_editor.sequences_all: if not scene.sequence_editor or not scene.sequence_editor.strips_all:
return return
#print("update_text_strips") #print("update_text_strips")
@@ -571,7 +616,7 @@ def update_text_strips(scene):
}) })
format_data.update(shot_strip.vsetb_strip_settings.format_data) format_data.update(shot_strip.vsetb_strip_settings.format_data)
for strip in scene.sequence_editor.sequences_all: for strip in scene.sequence_editor.strips_all:
if not strip.type == 'TEXT': if not strip.type == 'TEXT':
continue continue
@@ -585,14 +630,14 @@ def update_text_strips(scene):
strip.text = strip['text_pattern'].format_map(MissingKey(**format_data)) strip.text = strip['text_pattern'].format_map(MissingKey(**format_data))
def create_shot_strip(name, start, end): def create_shot_strip(name, start, end, channel='Shots'):
shot_strip = bpy.context.scene.sequence_editor.sequences.new_effect( shot_strip = bpy.context.scene.sequence_editor.strips.new_effect(
name, name,
'COLOR', 'COLOR',
get_channel_index('Shots'), get_channel_index(channel),
frame_start=start, frame_start=start,
frame_end=end length=end - start
) )
shot_strip.blend_alpha = 0 shot_strip.blend_alpha = 0
shot_strip.color = (0.5, 0.5, 0.5) shot_strip.color = (0.5, 0.5, 0.5)
+219 -126
View File
@@ -6,9 +6,9 @@ import bpy
from bpy.types import Panel, Menu from bpy.types import Panel, Menu
from bl_ui.utils import PresetPanel from bl_ui.utils import PresetPanel
from vse_toolbox.bl_utils import (get_addon_prefs, get_scene_settings, get_strip_settings) from vse_toolbox.bl_utils import get_addon_prefs, get_scene_settings, get_strip_settings
from vse_toolbox.constants import ASSET_PREVIEWS, REVIEW_TEMPLATE_BLEND from vse_toolbox.constants import ASSET_PREVIEWS, REVIEW_TEMPLATE_BLEND
from vse_toolbox.sequencer_utils import (set_active_strip, get_channel_name, get_strips) from vse_toolbox.sequencer_utils import set_active_strip, get_channel_name, get_strips
from vse_toolbox.file_utils import norm_str from vse_toolbox.file_utils import norm_str
@@ -30,7 +30,7 @@ class VSETB_PT_main(VSETB_main, Panel):
# row.label('VSE Toolbox') # row.label('VSE Toolbox')
# row.prop(settings, 'project_name', text='') # row.prop(settings, 'project_name', text='')
# project = settings.active_project # project = settings.active_project
# if project and project.type == 'TVSHOW': # if project and project.type == 'TVSHOW':
@@ -47,21 +47,23 @@ class VSETB_PT_main(VSETB_main, Panel):
row = layout.row(align=True) row = layout.row(align=True)
row.operator('vse_toolbox.load_projects', icon='FILE_REFRESH', text='', emboss=False) row.operator(
"vse_toolbox.load_projects", icon="FILE_REFRESH", text="", emboss=False
)
row.separator(factor=0.5) row.separator(factor=0.5)
row.prop(settings, 'project_name', text='') row.prop(settings, "project_name", text="")
project = settings.active_project project = settings.active_project
if not project: if not project:
return return
if project and project.type == 'TVSHOW': if project and project.type == "TVSHOW":
row.separator(factor=0.5) row.separator(factor=0.5)
row.prop(project, 'episode_name', text='') row.prop(project, "episode_name", text="")
row.separator(factor=0.5) row.separator(factor=0.5)
row.prop(project, "show_settings", icon="PREFERENCES", text='') row.prop(project, "show_settings", icon="PREFERENCES", text="")
if project.show_settings: if project.show_settings:
box = layout.box() box = layout.box()
split = box.split(factor=0.3) split = box.split(factor=0.3)
@@ -76,7 +78,7 @@ class VSETB_PT_main(VSETB_main, Panel):
row.separator(factor=0.5) row.separator(factor=0.5)
row.label(text="Template") row.label(text="Template")
row.operator("vse_toolbox.add_template", text="", icon='ADD', emboss=False) row.operator("vse_toolbox.add_template", text="", icon="ADD", emboss=False)
for i, template in enumerate(project.templates): for i, template in enumerate(project.templates):
row = name_col.row() row = name_col.row()
@@ -86,12 +88,13 @@ class VSETB_PT_main(VSETB_main, Panel):
subrow = row.row() subrow = row.row()
subrow.prop(template, "value", text="") subrow.prop(template, "value", text="")
row.separator(factor=0.25) row.separator(factor=0.25)
row.operator("vse_toolbox.remove_template", text="", icon='REMOVE', emboss=False).index = i row.operator(
"vse_toolbox.remove_template", text="", icon="REMOVE", emboss=False
).index = i
# settings = get_scene_settings() # settings = get_scene_settings()
# prefs = get_addon_prefs() # prefs = get_addon_prefs()
# project = settings.active_project # project = settings.active_project
# layout = self.layout # layout = self.layout
@@ -103,13 +106,13 @@ class VSETB_PT_main(VSETB_main, Panel):
# if project.type == 'TVSHOW': # if project.type == 'TVSHOW':
# col.prop(project, 'episode_name', text='Episodes') # col.prop(project, 'episode_name', text='Episodes')
#col.separator() # col.separator()
#row = col.row(align=True) # row = col.row(align=True)
#row.prop(settings, 'toogle_prefs', text='', icon='PREFERENCES', toggle=True) # row.prop(settings, 'toogle_prefs', text='', icon='PREFERENCES', toggle=True)
''' """
if settings.toogle_prefs: if settings.toogle_prefs:
box = col.box() box = col.box()
col = box.column(align=True) col = box.column(align=True)
@@ -126,21 +129,22 @@ class VSETB_PT_main(VSETB_main, Panel):
#col.prop(project, 'shot_template') #col.prop(project, 'shot_template')
# col.separator() # col.separator()
# col.operator('vse_toolbox.new_episode', text='Add Episode', icon='IMPORT') # col.operator('vse_toolbox.new_episode', text='Add Episode', icon='IMPORT')
''' """
# Rename # Rename
class VSETB_PT_strip(Panel): class VSETB_PT_strip(Panel):
bl_space_type = "SEQUENCE_EDITOR" bl_space_type = "SEQUENCE_EDITOR"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "Strip" bl_category = "Strip"
bl_label = "VSE ToolBox" bl_label = "VSE ToolBox"
#bl_order = 0 # bl_order = 0
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
strip = context.active_sequence_strip strip = context.active_strip
return strip and get_channel_name(strip) == 'Shots' return strip and get_channel_name(strip) == "Shots"
def draw(self, context): def draw(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
@@ -150,22 +154,28 @@ class VSETB_PT_strip(Panel):
layout.use_property_split = True layout.use_property_split = True
layout.use_property_decorate = False layout.use_property_decorate = False
layout.prop(settings, 'source_name', text='Source Name') layout.prop(settings, "source_name", text="Source Name")
class VSETB_PT_sequencer(VSETB_main, Panel): class VSETB_PT_sequencer(VSETB_main, Panel):
bl_label = "Sequencer" bl_label = "Sequencer"
#bl_parent_id = "VSETB_PT_main" # bl_parent_id = "VSETB_PT_main"
def draw_header_preset(self, context): def draw_header_preset(self, context):
settings = get_scene_settings() settings = get_scene_settings()
audio_strips = [s for s in get_strips('Audio') if s.type == "SOUND"] audio_strips = [s for s in get_strips("Audio") if s.type == "SOUND"]
depress = any(s.show_waveform for s in audio_strips) depress = any(s.show_waveform for s in audio_strips)
self.layout.operator('vse_toolbox.show_waveform', text="", icon="IPO_ELASTIC", depress=depress).enabled = not depress self.layout.operator(
"vse_toolbox.show_waveform", text="", icon="IPO_ELASTIC", depress=depress
).enabled = not depress
ico = ("RESTRICT_SELECT_OFF" if settings.auto_select_strip else "RESTRICT_SELECT_ON") ico = (
"RESTRICT_SELECT_OFF"
if settings.auto_select_strip
else "RESTRICT_SELECT_ON"
)
self.layout.prop(settings, "auto_select_strip", text="", icon=ico) self.layout.prop(settings, "auto_select_strip", text="", icon=ico)
def draw(self, context): def draw(self, context):
@@ -174,23 +184,36 @@ class VSETB_PT_sequencer(VSETB_main, Panel):
settings = get_scene_settings() settings = get_scene_settings()
project = settings.active_project project = settings.active_project
strip = context.active_sequence_strip strip = context.active_strip
channel = get_channel_name(strip) if strip is None and context.scene.sequence_editor:
strip = next(
(s for s in context.scene.sequence_editor.strips if s.select),
None,
)
channel = get_channel_name(strip) or "Selected Channel"
col = layout.column() col = layout.column()
col.operator('vse_toolbox.set_sequencer', text='Set-Up Sequencer', icon='SEQ_SEQUENCER') col.operator(
col.operator('vse_toolbox.auto_split', text='Auto Split Shots') "vse_toolbox.strips_rename", text=f"Rename {channel}", icon="SORTALPHA"
col.operator('vse_toolbox.strips_rename', text=f'Rename {channel}', icon='SORTALPHA') )
col.operator('vse_toolbox.set_stamps', text='Set Stamps', icon='COLOR') col.operator("vse_toolbox.set_stamps", text="Set Stamps", icon="COLOR")
col.operator("vse_toolbox.collect_files", text='Collect Files', icon='PACKAGE') col.operator("vse_toolbox.collect_files", text="Collect Files", icon="PACKAGE")
col.operator(
"vse_toolbox.scene_cut_detection",
text="Scene Cut Detection",
icon="SCULPTMODE_HLT",
)
col.operator(
"vse_toolbox.create_sequence_strip", text="Create Sequence", icon="SEQUENCE"
)
class VSETB_PT_settings(VSETB_main, Panel): class VSETB_PT_settings(VSETB_main, Panel):
bl_label = "Settings" bl_label = "Settings"
#bl_parent_id = "VSETB_PT_main" # bl_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'} bl_options = {"DEFAULT_CLOSED"}
#def draw_header_preset(self, context): # def draw_header_preset(self, context):
# self.layout.operator('vse_toolbox.import_files', icon='IMPORT', text='', emboss=False) # self.layout.operator('vse_toolbox.import_files', icon='IMPORT', text='', emboss=False)
def draw(self, context): def draw(self, context):
@@ -200,19 +223,21 @@ class VSETB_PT_settings(VSETB_main, Panel):
project = settings.active_project project = settings.active_project
col = layout.column() col = layout.column()
#row = col.row(align=True) # row = col.row(align=True)
col.prop(project, 'sequence_template') col.prop(project, "sequence_template")
col.prop(project, 'shot_template') col.prop(project, "shot_template")
col.prop(project, 'render_template') col.prop(project, "render_template")
class VSETB_PT_imports(VSETB_main, Panel): class VSETB_PT_imports(VSETB_main, Panel):
bl_label = "Imports" bl_label = "Imports"
#bl_parent_id = "VSETB_PT_main" # bl_parent_id = "VSETB_PT_main"
#bl_options = {'DEFAULT_CLOSED'} # bl_options = {'DEFAULT_CLOSED'}
def draw_header_preset(self, context): def draw_header_preset(self, context):
self.layout.operator('vse_toolbox.import_files', icon='IMPORT', text='', emboss=False) self.layout.operator(
"vse_toolbox.import_files", icon="IMPORT", text="", emboss=False
)
def draw(self, context): def draw(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
@@ -221,26 +246,35 @@ class VSETB_PT_imports(VSETB_main, Panel):
project = settings.active_project project = settings.active_project
col = layout.column() col = layout.column()
#row = col.row(align=True) # row = col.row(align=True)
#col.operator('vse_toolbox.import_files', text='Import', icon='IMPORT') # col.operator('vse_toolbox.import_files', text='Import', icon='IMPORT')
col.operator('vse_toolbox.import_spreadsheet', text='Import Spreadsheet', icon='SPREADSHEET') col.operator(
col.operator("vse_toolbox.import_shots", text='Import Shots', icon="FILE_MOVIE") "vse_toolbox.import_spreadsheet",
text="Import Spreadsheet",
icon="SPREADSHEET",
)
col.operator("vse_toolbox.import_shots", text="Import Shots", icon="FILE_MOVIE")
col.operator(
"vse_toolbox.import_stb_xml", text="Import STB XML", icon="FILE_TEXT"
)
class VSETB_PT_presets(PresetPanel, Panel): class VSETB_PT_presets(PresetPanel, Panel):
bl_label = 'Spreadsheet Presets' bl_label = "Spreadsheet Presets"
preset_subdir = 'vse_toolbox' preset_subdir = "vse_toolbox"
preset_operator = 'script.execute_preset' preset_operator = "script.execute_preset"
preset_add_operator = "vse_toolbox.add_spreadsheet_preset" preset_add_operator = "vse_toolbox.add_spreadsheet_preset"
class VSETB_PT_exports(VSETB_main, Panel): class VSETB_PT_exports(VSETB_main, Panel):
bl_label = "Exports" bl_label = "Exports"
#bl_parent_id = "VSETB_PT_main" # bl_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'} bl_options = {"DEFAULT_CLOSED"}
def draw_header_preset(self, context): def draw_header_preset(self, context):
self.layout.operator('vse_toolbox.export_spreadsheet', icon='EXPORT', text='', emboss=False) self.layout.operator(
"vse_toolbox.export_spreadsheet", icon="EXPORT", text="", emboss=False
)
def draw(self, context): def draw(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
@@ -250,43 +284,52 @@ class VSETB_PT_exports(VSETB_main, Panel):
col = layout.column(align=False) col = layout.column(align=False)
# TODO FAIRE DES VRAIS OPS # TODO FAIRE DES VRAIS OPS
col.operator('vse_toolbox.strips_render', text='Render Strips', icon='SEQUENCE') col.operator("vse_toolbox.strips_render", text="Render Strips", icon="SEQUENCE")
tracker_label = settings.tracker_name.title().replace('_', ' ') tracker_label = settings.tracker_name.title().replace("_", " ")
col.operator('vse_toolbox.upload_to_tracker', text=f'Upload to {tracker_label}', icon='EXPORT') col.operator(
col.operator('vse_toolbox.export_spreadsheet', text='Export Spreadsheet', icon='SPREADSHEET') "vse_toolbox.upload_to_tracker",
col.operator('vse_toolbox.export_edl', text='Export edl', icon='SEQ_SEQUENCER') text=f"Upload to {tracker_label}",
icon="EXPORT",
)
col.operator(
"vse_toolbox.export_spreadsheet",
text="Export Spreadsheet",
icon="SPREADSHEET",
)
col.operator("vse_toolbox.export_edl", text="Export edl", icon="SEQ_SEQUENCER")
class VSETB_PT_tracker(VSETB_main, Panel): class VSETB_PT_tracker(VSETB_main, Panel):
bl_label = "Tracker" bl_label = "Tracker"
#bl_parent_id = "VSETB_PT_main" # bl_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'} bl_options = {"DEFAULT_CLOSED"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_sequence_strip return context.active_strip
def draw_header_preset(self, context): def draw_header_preset(self, context):
active_strip = context.active_sequence_strip active_strip = context.active_strip
self.layout.label(text=active_strip.name) self.layout.label(text=active_strip.name)
def draw(self, context): def draw(self, context):
return return
class VSETB_PT_casting(VSETB_main, Panel): class VSETB_PT_casting(VSETB_main, Panel):
bl_label = "Casting" bl_label = "Casting"
bl_parent_id = "VSETB_PT_tracker" bl_parent_id = "VSETB_PT_tracker"
bl_options = {'DEFAULT_CLOSED'} bl_options = {"DEFAULT_CLOSED"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
strip = context.active_sequence_strip strip = context.active_strip
return strip and get_channel_name(strip) == 'Shots' return strip and get_channel_name(strip) == "Shots"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
settings = get_scene_settings() settings = get_scene_settings()
strip_settings = get_strip_settings() strip_settings = get_strip_settings()
@@ -297,33 +340,47 @@ class VSETB_PT_casting(VSETB_main, Panel):
if not project.assets: if not project.assets:
row = layout.row(align=True) row = layout.row(align=True)
row.label(text='No Assets in this Project') row.label(text="No Assets in this Project")
else: else:
row = layout.row() row = layout.row()
col = row.column() col = row.column()
col.template_list("VSETB_UL_casting", "shot_casting", strip_settings, "casting", strip_settings, "casting_index", rows=6) col.template_list(
"VSETB_UL_casting",
"shot_casting",
strip_settings,
"casting",
strip_settings,
"casting_index",
rows=6,
)
col_tool = row.column(align=True) col_tool = row.column(align=True)
col_tool.operator('vse_toolbox.casting_add', icon='ADD', text="") col_tool.operator("vse_toolbox.casting_add", icon="ADD", text="")
col_tool.operator('vse_toolbox.casting_remove', icon='REMOVE', text="") col_tool.operator("vse_toolbox.casting_remove", icon="REMOVE", text="")
col_tool.separator() col_tool.separator()
col_tool.operator('vse_toolbox.casting_move', icon='TRIA_UP', text="").direction = 'UP' col_tool.operator(
col_tool.operator('vse_toolbox.casting_move', icon='TRIA_DOWN', text="").direction = 'DOWN' "vse_toolbox.casting_move", icon="TRIA_UP", text=""
).direction = "UP"
col_tool.operator(
"vse_toolbox.casting_move", icon="TRIA_DOWN", text=""
).direction = "DOWN"
col_tool.separator() col_tool.separator()
col_tool.operator('vse_toolbox.copy_casting', icon='COPYDOWN', text="") col_tool.operator("vse_toolbox.copy_casting", icon="COPYDOWN", text="")
col_tool.operator('vse_toolbox.paste_casting', icon='PASTEDOWN', text="") col_tool.operator("vse_toolbox.paste_casting", icon="PASTEDOWN", text="")
col_tool.separator() col_tool.separator()
col_tool.operator('vse_toolbox.casting_replace', icon='ZOOM_ALL', text="") col_tool.operator("vse_toolbox.casting_replace", icon="ZOOM_ALL", text="")
col_tool.separator()
col_tool.operator("vse_toolbox.casting_create_asset", icon="PLUS", text="")
if strip_settings.casting: if strip_settings.casting:
casting_item = strip_settings.casting[strip_settings.casting_index] casting_item = strip_settings.casting[strip_settings.casting_index]
asset = casting_item.asset asset = casting_item.asset
if asset: if asset:
if asset.icon_id: if asset.icon_id:
row = col.row(align=True) row = col.row(align=True)
#row.scale_y = 0.5 # row.scale_y = 0.5
# box = col.box() # box = col.box()
# box.template_icon(icon_value=ico.icon_id, scale=7.5) # box.template_icon(icon_value=ico.icon_id, scale=7.5)
@@ -333,27 +390,27 @@ class VSETB_PT_casting(VSETB_main, Panel):
class VSETB_PT_metadata(VSETB_main, Panel): class VSETB_PT_metadata(VSETB_main, Panel):
bl_label = "Shot Metadata" bl_label = "Shot Metadata"
bl_parent_id = "VSETB_PT_tracker" bl_parent_id = "VSETB_PT_tracker"
bl_options = {'DEFAULT_CLOSED'} bl_options = {"DEFAULT_CLOSED"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_sequence_strip and get_scene_settings().active_project return context.active_strip and get_scene_settings().active_project
def draw(self, context): def draw(self, context):
strip_settings = get_strip_settings() strip_settings = get_strip_settings()
project = get_scene_settings().active_project project = get_scene_settings().active_project
layout = self.layout layout = self.layout
row = layout.row(align=True) row = layout.row(align=True)
label_col = row.column(align=True) label_col = row.column(align=True)
label_col.alignment = 'RIGHT' label_col.alignment = "RIGHT"
field_col = row.column(align=True) field_col = row.column(align=True)
for metadata_type in project.metadata_types: for metadata_type in project.metadata_types:
if metadata_type.entity_type != 'SHOT': if metadata_type.entity_type != "SHOT":
continue continue
metadata_key = metadata_type.field_name metadata_key = metadata_type.field_name
metadata_label = metadata_key.title() metadata_label = metadata_key.title()
@@ -363,45 +420,51 @@ class VSETB_PT_metadata(VSETB_main, Panel):
if metadata_type.choices: if metadata_type.choices:
metadata_value = getattr(strip_settings.metadata, metadata_key) metadata_value = getattr(strip_settings.metadata, metadata_key)
icon = 'LAYER_USED' icon = "LAYER_USED"
if metadata_value: if metadata_value:
if metadata_value in metadata_type.choices: if metadata_value in metadata_type.choices:
icon = 'DOT' icon = "DOT"
else: else:
icon = 'ADD' icon = "ADD"
field_row.prop_search(
strip_settings.metadata,
metadata_key,
metadata_type,
"choices",
results_are_suggestions=True,
icon=icon,
text="",
)
field_row.prop_search(strip_settings.metadata, metadata_key, metadata_type, 'choices',
results_are_suggestions=True, icon=icon, text='')
else: else:
field_row.prop(strip_settings.metadata, metadata_key, text='') field_row.prop(strip_settings.metadata, metadata_key, text="")
class VSETB_PT_comments(VSETB_main, Panel): class VSETB_PT_comments(VSETB_main, Panel):
bl_label = "Comments" bl_label = "Comments"
bl_parent_id = "VSETB_PT_tracker" bl_parent_id = "VSETB_PT_tracker"
bl_options = {'DEFAULT_CLOSED'} bl_options = {"DEFAULT_CLOSED"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_sequence_strip and get_scene_settings().active_project return context.active_strip and get_scene_settings().active_project
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
row = layout.row(align=True) row = layout.row(align=True)
label_col = row.column(align=True) label_col = row.column(align=True)
label_col.alignment = 'RIGHT' label_col.alignment = "RIGHT"
field_col = row.column(align=True) field_col = row.column(align=True)
strip_settings = get_strip_settings() strip_settings = get_strip_settings()
project = get_scene_settings().active_project project = get_scene_settings().active_project
for task_type in project.task_types: for task_type in project.task_types:
norm_task_name = norm_str(task_type.name) norm_task_name = norm_str(task_type.name)
if not hasattr(strip_settings.tasks, norm_task_name): if not hasattr(strip_settings.tasks, norm_task_name):
continue continue
@@ -410,19 +473,22 @@ class VSETB_PT_comments(VSETB_main, Panel):
row = field_col.row(align=True) row = field_col.row(align=True)
row.separator() row.separator()
sub = row.row(align=True) sub = row.row(align=True)
sub.alignment = 'LEFT' sub.alignment = "LEFT"
sub.scale_x = 0.15 sub.scale_x = 0.15
sub.prop(task_type, 'color', text='') sub.prop(task_type, "color", text="")
sub.enabled = False sub.enabled = False
row.prop(getattr(strip_settings.tasks, norm_task_name), 'comment', text='') row.prop(getattr(strip_settings.tasks, norm_task_name), "comment", text="")
def context_menu_prop(self, context): def context_menu_prop(self, context):
if not hasattr(context, 'button_prop') or context.space_data.type != 'SEQUENCE_EDITOR': if (
not hasattr(context, "button_prop")
or context.space_data.type != "SEQUENCE_EDITOR"
):
return return
settings = get_strip_settings() settings = get_strip_settings()
if not settings: if not settings:
return return
@@ -430,10 +496,12 @@ def context_menu_prop(self, context):
button_prop = context.button_prop button_prop = context.button_prop
if button_prop not in settings.metadata.bl_rna.properties.values(): if button_prop not in settings.metadata.bl_rna.properties.values():
return return
layout = self.layout layout = self.layout
layout.separator() layout.separator()
layout.operator('vse_toolbox.copy_metadata', icon='PASTEDOWN', text='Copy metadata to selected').metadata = button_prop.name layout.operator(
"vse_toolbox.copy_metadata", icon="PASTEDOWN", text="Copy metadata to selected"
).metadata = button_prop.name
class VSETB_MT_main_menu(Menu): class VSETB_MT_main_menu(Menu):
@@ -442,26 +510,49 @@ class VSETB_MT_main_menu(Menu):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
op = layout.operator('workspace.append_activate', text='Set Review Workspace', icon="WORKSPACE") op = layout.operator(
op.idname = 'Review' "workspace.append_activate", text="Set Review Workspace", icon="WORKSPACE"
)
op.idname = "Review"
op.filepath = str(REVIEW_TEMPLATE_BLEND) op.filepath = str(REVIEW_TEMPLATE_BLEND)
layout.operator("wm.split_view", icon="ARROW_LEFTRIGHT") layout.operator("wm.split_view", icon="ARROW_LEFTRIGHT")
layout.operator('vse_toolbox.open_strip_folder', text='Open Strip Folder', icon='FILE_FOLDER')
layout.operator('vse_toolbox.open_shot_on_tracker', text='Open Shot on Tracker', icon='URL')
layout.separator() layout.separator()
layout.operator('vse_toolbox.insert_channel', text='Insert Channel', icon='TRIA_UP_BAR') layout.operator(
layout.operator('vse_toolbox.remove_channel', text='Remove Channel', icon='TRIA_DOWN_BAR') "vse_toolbox.update_media", text="Update Media", icon="FILE_REFRESH"
)
layout.operator(
"vse_toolbox.open_shot_on_tracker", text="Open Shot on Tracker", icon="URL"
)
layout.operator(
"vse_toolbox.open_strip_folder",
text="Open Strip Folder",
icon="FILE_FOLDER",
)
layout.separator() layout.separator()
layout.operator('vse_toolbox.merge_shot_strips', text='Merge Shots') layout.operator(
"vse_toolbox.insert_channel", text="Insert Channel", icon="TRIA_UP_BAR"
)
layout.operator(
"vse_toolbox.remove_channel", text="Remove Channel", icon="TRIA_DOWN_BAR"
)
layout.separator()
layout.operator("vse_toolbox.merge_shot_strips", text="Merge Shots")
layout.operator("vse_toolbox.create_sequence_strip", text="Create Sequence")
layout.operator(
"vse_toolbox.scene_cut_detection",
text="Scene Cut Detection",
icon="SCULPTMODE_HLT",
)
def draw_vse_toolbox_menu(self, context): def draw_vse_toolbox_menu(self, context):
self.layout.menu("VSETB_MT_main_menu") self.layout.menu("VSETB_MT_main_menu")
def draw_file_new(self, context): def draw_file_new(self, context):
self.layout.separator() self.layout.separator()
op = self.layout.operator('wm.read_homefile', text="Review") op = self.layout.operator("wm.read_homefile", text="Review")
op.filepath = str(REVIEW_TEMPLATE_BLEND) op.filepath = str(REVIEW_TEMPLATE_BLEND)
op.load_ui = True op.load_ui = True
@@ -477,23 +568,25 @@ classes = (
VSETB_PT_presets, VSETB_PT_presets,
VSETB_PT_exports, VSETB_PT_exports,
VSETB_PT_strip, VSETB_PT_strip,
VSETB_MT_main_menu VSETB_MT_main_menu,
) )
def register():
def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.UI_MT_button_context_menu.append(context_menu_prop) bpy.types.UI_MT_button_context_menu.append(context_menu_prop)
bpy.types.SEQUENCER_MT_editor_menus.append(draw_vse_toolbox_menu) bpy.types.SEQUENCER_MT_editor_menus.append(draw_vse_toolbox_menu)
bpy.types.TOPBAR_MT_file_new.append(draw_file_new) bpy.types.TOPBAR_MT_file_new.append(draw_file_new)
def unregister(): def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
bpy.types.UI_MT_button_context_menu.remove(context_menu_prop) bpy.types.UI_MT_button_context_menu.remove(context_menu_prop)
bpy.types.SEQUENCER_MT_editor_menus.remove(draw_vse_toolbox_menu) bpy.types.SEQUENCER_MT_editor_menus.remove(draw_vse_toolbox_menu)
bpy.types.TOPBAR_MT_file_new.remove(draw_file_new) bpy.types.TOPBAR_MT_file_new.remove(draw_file_new)
+23 -2
View File
@@ -64,6 +64,18 @@ def load_trackers():
print(e) print(e)
def _load_settings_after_startup():
"""Apply the config once Blender has a normal context available."""
if not getattr(bpy.context, 'scene', None):
return 0.5
try:
bpy.ops.vse_toolbox.load_settings()
except RuntimeError as e:
print(f'Could not load VSE Toolbox settings at startup: {e}')
return None
def load_prefs(): def load_prefs():
prefs = get_addon_prefs() prefs = get_addon_prefs()
prefs_config_file = prefs.config_path prefs_config_file = prefs.config_path
@@ -91,7 +103,16 @@ def load_prefs():
continue continue
setattr(tracker_pref, k, os.path.expandvars(v)) setattr(tracker_pref, k, os.path.expandvars(v))
prefs['tracker_name'] = prefs_datas['tracker_name'] try:
settings = get_scene_settings()
settings.tracker_name = prefs_datas['tracker_name']
except AttributeError:
pass # Scene not available yet during register()
# Loading preferences only sets the config path and tracker credentials;
# apply the project configuration once Blender has finished registering
# and provides a normal context (register() uses _RestrictContext).
bpy.app.timers.register(_load_settings_after_startup, first_interval=0.1)
class Trackers(PropertyGroup): class Trackers(PropertyGroup):
@@ -155,7 +176,7 @@ def register():
config_path = os.getenv('VSE_TOOLBOX_CONFIG') config_path = os.getenv('VSE_TOOLBOX_CONFIG')
if config_path: if config_path:
prefs['config_path'] = os.path.expandvars(config_path) prefs.config_path = os.path.expandvars(config_path)
load_prefs() load_prefs()
+32 -8
View File
@@ -119,6 +119,7 @@ class Sequence(PropertyGroup):
class TaskStatus(PropertyGroup): class TaskStatus(PropertyGroup):
is_done : BoolProperty(default=False)
__annotations__ = {} __annotations__ = {}
@@ -252,6 +253,7 @@ class ImportShots(PropertyGroup):
#shot_folder_template: StringProperty( #shot_folder_template: StringProperty(
# name="Shot Template", default="$PROJECT_ROOT/sequences/sq{sequence}/sh{shot}") # name="Shot Template", default="$PROJECT_ROOT/sequences/sq{sequence}/sh{shot}")
clear: BoolProperty(default=True)
video_template : StringProperty( video_template : StringProperty(
name="Video Path", default="//sources/{sequence}_{shot}_{task}.{ext}") name="Video Path", default="//sources/{sequence}_{shot}_{task}.{ext}")
@@ -266,7 +268,8 @@ class ImportShots(PropertyGroup):
class UploadToTracker(PropertyGroup): class UploadToTracker(PropertyGroup):
render_strips: BoolProperty(default=False) render_strips: BoolProperty(default=False,
description='Render each selected shot strip to the Movie Path before uploading it as the preview')
render_strip_template : StringProperty( render_strip_template : StringProperty(
name="Movie Path", default="//render/{strip}.{ext}") name="Movie Path", default="//render/{strip}.{ext}")
@@ -298,7 +301,28 @@ def get_episodes_items(self, context):
def on_episode_updated(self, context): def on_episode_updated(self, context):
settings = get_scene_settings() settings = get_scene_settings()
os.environ['TRACKER_EPISODE_ID'] = settings.active_episode.id project = settings.active_project
episode = settings.active_episode
if not episode or not episode.id:
return
os.environ['TRACKER_EPISODE_ID'] = episode.id
# Reload sequences for the selected episode (TVSHOW workflow)
prefs = get_addon_prefs()
tracker = prefs.tracker
if not tracker or not project:
return
project.sequences.clear()
try:
for seq in tracker.get_sequences(project.id, episode=episode.id):
item = project.sequences.add()
item.name = seq['name']
item.id = seq['id']
except Exception as e:
print(f'Could not load sequences for episode {episode.name}: {e}')
class Project(PropertyGroup): class Project(PropertyGroup):
id : StringProperty(default='') id : StringProperty(default='')
@@ -714,7 +738,7 @@ class VSETB_PGT_strip_settings(PropertyGroup):
@property @property
def strip(self): def strip(self):
sequences = bpy.context.scene.sequence_editor.sequences_all sequences = bpy.context.scene.sequence_editor.strips_all
return next(s for s in sequences if s.vsetb_strip_settings == self) return next(s for s in sequences if s.vsetb_strip_settings == self)
@property @property
@@ -727,7 +751,7 @@ class VSETB_PGT_strip_settings(PropertyGroup):
channel = get_channel_name(strip) channel = get_channel_name(strip)
if channel == 'Sequences': if channel == 'Sequences':
data = parse(project.sequence_template, strip.name) data = parse(project.sequence_template, strip.name) or {}
#data['index'] = int(data['index']) #data['index'] = int(data['index'])
#data['sequence'] = strip.name #data['sequence'] = strip.name
data['strip'] = strip.name data['strip'] = strip.name
@@ -736,10 +760,10 @@ class VSETB_PGT_strip_settings(PropertyGroup):
elif channel == "Shots": elif channel == "Shots":
data = {} data = {}
if sequence_strip_name := get_strip_sequence_name(strip): if sequence_strip_name := get_strip_sequence_name(strip):
data = parse(project.sequence_template, sequence_strip_name) data = parse(project.sequence_template, sequence_strip_name) or {}
data['sequence_strip'] = sequence_strip_name data['sequence_strip'] = sequence_strip_name
data.update(parse(project.shot_template, strip.name)) data.update(parse(project.shot_template, strip.name) or {})
#data['index'] = int(data['index']) #data['index'] = int(data['index'])
data['strip'] = strip.name data['strip'] = strip.name
@@ -807,7 +831,7 @@ def register():
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.Scene.vsetb_settings = PointerProperty(type=VSETB_PGT_scene_settings) bpy.types.Scene.vsetb_settings = PointerProperty(type=VSETB_PGT_scene_settings)
bpy.types.Sequence.vsetb_strip_settings = PointerProperty(type=VSETB_PGT_strip_settings) bpy.types.Strip.vsetb_strip_settings = PointerProperty(type=VSETB_PGT_strip_settings)
#load_metadata_types() #load_metadata_types()
bpy.app.handlers.load_post.append(load_handler) bpy.app.handlers.load_post.append(load_handler)
@@ -817,7 +841,7 @@ def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
del bpy.types.Sequence.vsetb_strip_settings del bpy.types.Strip.vsetb_strip_settings
del bpy.types.Scene.vsetb_settings del bpy.types.Scene.vsetb_settings
bpy.app.handlers.load_post.remove(load_handler) bpy.app.handlers.load_post.remove(load_handler)