From d2d64dc8a8c509f5db84c6d52e83672c54cf2005 Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Tue, 10 Mar 2026 16:09:03 +0100 Subject: [PATCH] Blender 5.0 compat + STB XML import + sequence/stamps improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 - 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 --- __init__.py | 18 ++-- bl_utils.py | 3 +- file_utils.py | 4 +- operators/casting.py | 87 +++++++++++++--- operators/imports.py | 194 ++++++++++++++++++++++++++++++++++-- operators/sequencer.py | 178 +++++++++++++++++++++++++-------- operators/spreadsheet.py | 4 +- operators/tracker.py | 44 ++++++-- resources/trackers/kitsu.py | 29 +++++- scene_cut_detection.py | 2 +- sequencer_utils.py | 52 ++++++---- ui/panels.py | 18 ++-- ui/preferences.py | 8 +- ui/properties.py | 29 +++++- 14 files changed, 548 insertions(+), 122 deletions(-) diff --git a/__init__.py b/__init__.py index 5c2173b..9c04d93 100644 --- a/__init__.py +++ b/__init__.py @@ -36,28 +36,34 @@ import bpy def register(): - bpy.app.handlers.frame_change_post.append(update_text_strips) - bpy.app.handlers.render_pre.append(update_text_strips) + if update_text_strips not in bpy.app.handlers.frame_change_post: + 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: 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() #print('\n\n-------------------', prefs.config_path) def unregister(): - bpy.app.handlers.frame_change_post.remove(update_text_strips) - bpy.app.handlers.render_pre.remove(update_text_strips) + if update_text_strips in bpy.app.handlers.frame_change_post: + 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: bpy.utils.previews.remove(ASSET_PREVIEWS) except Exception as e: 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): module.unregister() diff --git a/bl_utils.py b/bl_utils.py index 4694fe7..d03ed30 100644 --- a/bl_utils.py +++ b/bl_utils.py @@ -49,8 +49,7 @@ def get_scene_settings(): return bpy.context.scene.vsetb_settings def get_strip_settings(): - scn = bpy.context.scene - strip = bpy.context.active_sequence_strip + strip = bpy.context.active_strip if not strip: return diff --git a/file_utils.py b/file_utils.py index b59f18e..2903506 100644 --- a/file_utils.py +++ b/file_utils.py @@ -54,7 +54,7 @@ def norm_str(string, separator='_', format=str.lower, padding=0): string = string.replace('_', ' ') string = string.replace('-', ' ') string = re.sub('[ ]+', ' ', string) - string = re.sub('[ ]+\/[ ]+', '/', string) + string = re.sub(r'[ ]+/[ ]+', '/', string) string = string.strip() if format: @@ -74,7 +74,7 @@ def norm_name(string, separator='_', format=str.lower, padding=0): string = string.replace('_', ' ') string = string.replace('-', ' ') string = re.sub('[ ]+', ' ', string) - string = re.sub('[ ]+\/[ ]+', '/', string) + string = re.sub(r'[ ]+/[ ]+', '/', string) string = string.strip() if format: diff --git a/operators/casting.py b/operators/casting.py index 6cd68e5..5031d03 100644 --- a/operators/casting.py +++ b/operators/casting.py @@ -53,7 +53,7 @@ class VSETB_OT_casting_replace(Operator): item = self.assets.add() item.name = asset.tracker_name - strip = context.active_sequence_strip + strip = context.active_strip asset_casting_index = strip.vsetb_strip_settings.casting_index 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', [])) @classmethod def poll(cls, context): - active_strip = context.active_sequence_strip + active_strip = context.active_strip if active_strip: return True @@ -135,7 +135,7 @@ class VSETB_OT_casting_remove(Operator): @classmethod def poll(cls, context): - active_strip = context.active_sequence_strip + active_strip = context.active_strip if active_strip: return True @@ -190,7 +190,7 @@ class VSETB_OT_casting_move(Operator): @classmethod def poll(cls, context): - active_strip = context.active_sequence_strip + active_strip = context.active_strip if active_strip: return True @@ -231,7 +231,7 @@ class VSETB_OT_copy_casting(Operator): def poll(cls, context): if not context.scene.sequence_editor: return - active_strip = context.active_sequence_strip + active_strip = context.active_strip strip_settings = get_strip_settings() if active_strip and strip_settings.casting: @@ -256,7 +256,7 @@ class VSETB_OT_paste_casting(Operator): @classmethod def poll(cls, context): - return context.active_sequence_strip + return context.active_strip def invoke(self, context, event): self.mode = 'REPLACE' @@ -279,7 +279,7 @@ class VSETB_OT_paste_casting(Operator): casting_datas = json.loads(CASTING_BUFFER.read_text()) 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 if self.mode == 'REPLACE': @@ -317,7 +317,7 @@ class VSETB_OT_copy_metadata(Operator): @classmethod 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): prefs = get_addon_prefs() @@ -329,11 +329,11 @@ class VSETB_OT_copy_metadata(Operator): if not 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) - for strip in context.selected_sequences: - if strip == context.active_sequence_strip: + for strip in context.selected_strips: + if strip == context.active_strip: continue setattr(strip.vsetb_strip_settings.metadata, metadata, metadata_value) @@ -341,6 +341,68 @@ class VSETB_OT_copy_metadata(Operator): return {"FINISHED"} +def get_asset_type_items(self, context): + settings = get_scene_settings() + project = settings.active_project + if project and project.asset_types: + return [(t.name, t.name, '') for t in project.asset_types] + return [('Character', 'Character', '')] + + +class VSETB_OT_casting_create_asset(Operator): + bl_idname = "vse_toolbox.casting_create_asset" + bl_label = "Create & Cast Asset" + bl_description = "Create a new asset in Kitsu and add to casting" + bl_options = {"REGISTER", "UNDO"} + + asset_name: StringProperty(name="Name") + asset_type: EnumProperty(name="Type", items=get_asset_type_items) + description: StringProperty(name="Description") + + @classmethod + def poll(cls, context): + settings = get_scene_settings() + return settings.active_project + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + prefs = get_addon_prefs() + settings = get_scene_settings() + project = settings.active_project + tracker = prefs.tracker + + try: + new_asset_data = tracker.new_asset( + self.asset_name, self.asset_type, + project=project.id, description=self.description + ) + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + + # Add to local project assets + asset = project.assets.add() + asset.name = new_asset_data['id'] + asset.id = new_asset_data['id'] + asset.tracker_name = new_asset_data['name'] + asset.asset_type = self.asset_type + + # Cast to selected shot strips + strips = get_strips('Shots', selected_only=True) + for strip in strips: + strip_settings = strip.vsetb_strip_settings + cast_item = strip_settings.casting.add() + cast_item.name = new_asset_data['id'] + cast_item.id = new_asset_data['id'] + cast_item['_name'] = self.asset_name + strip_settings.casting.update() + + self.report({'INFO'}, f"Created asset '{self.asset_name}' and cast to {len(strips)} shots") + return {'FINISHED'} + + classes = ( VSETB_OT_casting_add, VSETB_OT_casting_remove, @@ -348,7 +410,8 @@ classes = ( VSETB_OT_copy_casting, VSETB_OT_paste_casting, VSETB_OT_casting_replace, - VSETB_OT_copy_metadata + VSETB_OT_copy_metadata, + VSETB_OT_casting_create_asset, ) def register(): diff --git a/operators/imports.py b/operators/imports.py index 79dcef2..d51e380 100644 --- a/operators/imports.py +++ b/operators/imports.py @@ -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, 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 @@ -90,6 +90,15 @@ class VSETB_OT_import_files(Operator): import_edit : BoolProperty(name='', default=True) 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', '')]) import_movie : BoolProperty(name='', default=False) @@ -119,6 +128,8 @@ class VSETB_OT_import_files(Operator): sub.active = self.import_edit sub.prop(self, 'edit') row = layout.row(align=True) + row.prop(self, 'edit_adapter', text='Format') + row = layout.row(align=True) row.prop(self, 'match_by', expand=True) layout.separator() @@ -144,7 +155,7 @@ class VSETB_OT_import_files(Operator): return {'RUNNING_MODAL'} def execute(self, context): - sequencer = context.scene.sequence_editor.sequences + sequencer = context.scene.sequence_editor.strips edit_filepath = Path(self.directory, self.edit) if not edit_filepath.exists(): @@ -168,7 +179,12 @@ class VSETB_OT_import_files(Operator): if self.import_edit: 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: 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)}') import_sound(movie_filepath) - context.scene.sequence_editor.sequences.update() + context.scene.sequence_editor.strips.update() return {"FINISHED"} @@ -442,8 +458,9 @@ class VSETB_OT_import_shots(Operator): for asset_data in casting_data: item = strip_settings.casting.add() - item.name = asset_data['asset_name'] + item.name = asset_data['asset_name'] item.id = asset_data['asset_id'] + item.instance = asset_data.get('nb_occurences', 1) item['_name'] = asset_data['asset_name'] strip_settings.casting.update() @@ -461,6 +478,10 @@ class VSETB_OT_import_shots(Operator): task_types = [t for t in project.task_types if t.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 if import_shots.clear: @@ -484,12 +505,12 @@ class VSETB_OT_import_shots(Operator): frames = int(frames) frame_end = frame_index + frames - strip = scn.sequence_editor.sequences.new_effect( + strip = scn.sequence_editor.strips.new_effect( name=shot_data['name'], type='COLOR', channel=get_channel_index('Shots'), frame_start=frame_index, - frame_end=frame_index + frames + length=frames ) strip.blend_alpha = 0 strip.color = (0.5, 0.5, 0.5) @@ -534,12 +555,12 @@ class VSETB_OT_import_shots(Operator): frame_index += frames - strip = scn.sequence_editor.sequences.new_effect( + strip = scn.sequence_editor.strips.new_effect( name=sequence.name, type='COLOR', channel=get_channel_index('Sequences'), frame_start=sequence_start, - frame_end=frame_index + length=frame_index - sequence_start ) strip.blend_alpha = 0 strip.color = (0.25, 0.25, 0.25) @@ -673,6 +694,160 @@ class VSETB_OT_import_shots(Operator): 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 ) + 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 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 = ( VSETB_OT_select_sequence, VSETB_OT_unselect_sequence, @@ -683,6 +858,7 @@ classes = ( VSETB_UL_import_task, VSETB_OT_auto_select_files, VSETB_OT_import_files, + VSETB_OT_import_stb_xml, VSETB_OT_import_shots, ) diff --git a/operators/sequencer.py b/operators/sequencer.py index c4c2c2f..803960d 100644 --- a/operators/sequencer.py +++ b/operators/sequencer.py @@ -35,7 +35,7 @@ class VSETB_OT_rename(Operator): @classmethod def poll(cls, context): settings = get_scene_settings() - strip = context.active_sequence_strip + strip = context.active_strip return settings.active_project and get_channel_name(strip) in ('Shots', 'Sequences') def invoke(self, context, event): @@ -54,7 +54,7 @@ class VSETB_OT_rename(Operator): sequence = str(project.sequence_start_number).zfill(project.sequence_padding) shot = str(project.shot_start_number).zfill(project.shot_padding) - strip = context.active_sequence_strip + strip = context.active_strip channel_name = get_channel_name(strip) col = layout.column() @@ -89,7 +89,7 @@ class VSETB_OT_rename(Operator): settings = get_scene_settings() project = settings.active_project - strip = context.active_sequence_strip + strip = context.active_strip channel_name = get_channel_name(strip) strips = get_strips(channel=channel_name, selected_only=self.selected_only) @@ -208,15 +208,15 @@ class VSETB_OT_scene_cut_detection(Operator): self.destination_channel_name = 'Shots' # Select active channel by default - if (strip := context.active_sequence_strip) and get_channel_name(strip) != 'Shots': + if (strip := context.active_strip) and get_channel_name(strip) != 'Shots': self.source_channel_name = get_channel_name(strip) self.frame_start = context.scene.frame_start self.frame_end = context.scene.frame_end - if context.selected_sequences: - self.frame_start = min([s.frame_final_start for s in context.selected_sequences]) - self.frame_end = max([s.frame_final_end for s in context.selected_sequences]) + 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) @@ -318,12 +318,22 @@ class VSETB_OT_set_stamps(Operator): scn = context.scene settings = get_scene_settings() project = settings.active_project - #strip_settings = get_strip_settings() - channel_index = get_channel_index('Stamps') + # Remove existing stamps for strip in get_strips('Stamps'): 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') @@ -336,7 +346,9 @@ class VSETB_OT_set_stamps(Operator): crop_x = int(width * 0.4) 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, font_size=font_size, y=margin, box_margin=box_margin, select=True, box_color=(0, 0, 0, 0.5)) @@ -344,33 +356,32 @@ class VSETB_OT_set_stamps(Operator): # Project Name project_text = '{project}' if project.type == 'TVSHOW': - project_text = '{project} / ep{episode}' - project_strip_stamp = new_text_strip('project_stamp', channel=1, **stamp_params, - text=project_text, x=0.01, align_x='LEFT', align_y='BOTTOM') + project_text = '{project} / ep {episode}' + project_strip_stamp = new_text_strip('project_stamp', channel=tmp_base, **stamp_params, + 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_y = crop_max_y # Shot Name - - shot_strip_stamp = new_text_strip('shot_stamp', channel=2, **stamp_params, - text='sq{sequence} / sh{shot}', align_y='BOTTOM') + shot_strip_stamp = new_text_strip('shot_stamp', channel=tmp_base + 1, **stamp_params, + text='{sequence_strip} / {strip}', anchor_y='BOTTOM') shot_strip_stamp.crop.min_x = crop_x shot_strip_stamp.crop.max_x = crop_x shot_strip_stamp.crop.max_y = crop_max_y # Frame - frame_strip_stamp = new_text_strip('frame_stamp', channel=3, **stamp_params, - text='{shot_frame} / {shot_duration} {timecode}', x=0.99, align_x='RIGHT', align_y='BOTTOM') + frame_strip_stamp = new_text_strip('frame_stamp', channel=tmp_base + 2, **stamp_params, + 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.max_y = crop_max_y bpy.ops.sequencer.meta_make() - stamps_strip = context.active_sequence_strip + stamps_strip = context.active_strip 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.channel = get_channel_index('Stamps') @@ -397,7 +408,7 @@ class VSETB_OT_previous_shot(Operator): active_strip_index = strips.index(active_strip) next_shot = strips[active_strip_index - 1] context.scene.frame_set(next_shot.frame_final_start) - + bpy.ops.sequencer.select_all(action="DESELECT") next_shot.select = True context.scene.sequence_editor.active_strip = next_shot @@ -424,10 +435,10 @@ class VSETB_OT_next_shot(Operator): active_strip_index = strips.index(active_strip) next_shot = strips[active_strip_index + 1] context.scene.frame_set(next_shot.frame_final_start) - + bpy.ops.sequencer.select_all(action="DESELECT") next_shot.select = True - context.active_sequence_strip = next_shot + context.scene.sequence_editor.active_strip = next_shot return {"FINISHED"} @@ -440,7 +451,7 @@ class VSETB_OT_open_strip_folder(Operator): @classmethod def poll(cls, context): - strip = context.active_sequence_strip + strip = context.active_strip if not strip: cls.poll_message_set('No active') return @@ -458,7 +469,7 @@ class VSETB_OT_open_strip_folder(Operator): 'Sequences': 'sequence_dir' } - strip = context.active_sequence_strip + strip = context.active_strip settings = get_scene_settings() project = settings.active_project @@ -501,12 +512,12 @@ class VSETB_OT_collect_files(Operator): cls.poll_message_set('Save the blend to collect files') def execute(self, context): - strip = context.active_sequence_strip + strip = context.active_strip settings = get_scene_settings() project = settings.active_project - strips = [s for s in context.scene.sequence_editor.sequences_all if s.type in ('MOVIE', 'SOUND')] + 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)) for i, strip in enumerate(strips): @@ -556,18 +567,18 @@ class VSETB_OT_insert_channel(Operator): @classmethod def poll(cls, context): - return context.active_sequence_strip + return context.active_strip def execute(self, context): 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): if strip.channel >= channel_index: strip.channel += 1 - + channels = {i: (c.name, c.lock, c.mute) for i, c in enumerate(scn.sequence_editor.channels)} for i in sorted(channels.keys()): channel = scn.sequence_editor.channels[i] @@ -599,16 +610,16 @@ class VSETB_OT_remove_channel(Operator): @classmethod def poll(cls, context): - return context.active_sequence_strip + return context.active_strip def execute(self, context): 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") - strips = list(scn.sequence_editor.sequences) + strips = list(scn.sequence_editor.strips) for strip in sorted(strips, key=lambda x: x.channel): if strip.channel >= channel_index: @@ -709,7 +720,7 @@ class VSETB_OT_merge_shot_strips(Operator): @classmethod def poll(cls, context): - selected_strips = bpy.context.selected_sequences + selected_strips = bpy.context.selected_strips if len(selected_strips) <= 1: return False @@ -717,16 +728,100 @@ class VSETB_OT_merge_shot_strips(Operator): def execute(self, context): - selected_strips = bpy.context.selected_sequences + selected_strips = bpy.context.selected_strips last_frame = selected_strips[-1].frame_final_end for i in range(1, len(selected_strips)): - context.scene.sequence_editor.sequences.remove(selected_strips[i]) + context.scene.sequence_editor.strips.remove(selected_strips[i]) selected_strips[0].frame_final_end = last_frame 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="SC010", + ) + + @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): + 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" @@ -735,7 +830,7 @@ class VSETB_OT_update_media(Operator): @classmethod def poll(cls, context): - if not (strip := context.active_sequence_strip) or strip.type not in ('MOVIE', 'SOUND'): + if not (strip := context.active_strip) or strip.type not in ('MOVIE', 'SOUND'): cls.poll_message_set('No active AUDIO or MOVIE strips') return @@ -744,7 +839,7 @@ class VSETB_OT_update_media(Operator): return True def execute(self, context): - for strip in context.selected_sequences: + for strip in context.selected_strips: current_movie = Path(abspath(bpy.path.abspath(strip.filepath))) pattern_name = re.sub(r'[^\s._\?]+', '*', current_movie.name) @@ -801,6 +896,7 @@ classes = ( VSETB_OT_insert_channel, VSETB_OT_remove_channel, VSETB_OT_merge_shot_strips, + VSETB_OT_create_sequence_strip, VSETB_OT_update_media, WM_OT_split_view, ) diff --git a/operators/spreadsheet.py b/operators/spreadsheet.py index 67be5f0..1b8b5b2 100644 --- a/operators/spreadsheet.py +++ b/operators/spreadsheet.py @@ -341,7 +341,7 @@ class VSETB_OT_import_spreadsheet(Operator): settings = get_scene_settings() project = settings.active_project spreadsheet = project.spreadsheet_import - sequencer = scn.sequence_editor.sequences + sequencer = scn.sequence_editor.strips assets_missing = set() @@ -397,7 +397,7 @@ class VSETB_OT_import_spreadsheet(Operator): type='COLOR', channel=channel, frame_start=frame_start, - frame_end=frame_end, + length=frame_end - frame_start, ) strip.blend_alpha = 0.0 diff --git a/operators/tracker.py b/operators/tracker.py index 4750843..be3e6a6 100644 --- a/operators/tracker.py +++ b/operators/tracker.py @@ -148,18 +148,27 @@ class VSETB_OT_load_projects(Operator): episode_datas = tracker.get_episodes(project_data) for episode_data in episode_datas: episode = project.episodes.get(episode_data['name']) - + if not episode: episode = project.episodes.add() episode.name = episode_data['name'] episode.id = episode_data['id'] - + # Clear deleted episodes ep_names = [e['name'] for e in episode_datas] for ep in reversed(project.episodes): if ep.name not in ep_names: 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: # Add sequences sequences_data = tracker.get_sequences(project_data) @@ -257,15 +266,24 @@ class VSETB_OT_new_episode(Operator): settings = get_scene_settings() prefs = get_addon_prefs() tracker = prefs.tracker + project = settings.active_project - episode_name = settings.episode_template.format(index=int(self.episode_name)) - episode = tracker.get_episode(episode_name) - if episode: + if not project: + self.report({'ERROR'}, 'No active project') + 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') return {"CANCELLED"} - - tracker.new_episode(episode_name) - tracker.update_project() + + episode_data = tracker.new_episode(episode_name, project=project.id) + + # 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') @@ -355,6 +373,9 @@ class VSETB_OT_upload_to_tracker(Operator): context.window_manager.progress_update(i) strip_settings = strip.vsetb_strip_settings 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 sequence = tracker.get_sequence(sequence_name, episode=episode) metadata = strip_settings.metadata.to_dict(use_name=False) @@ -401,6 +422,7 @@ class VSETB_OT_upload_to_tracker(Operator): preview = None comment_data = None + preview_data = None if status or upload_to_tracker.comment or preview: comment_data = tracker.new_comment(task, comment=upload_to_tracker.comment, status=status) if preview: @@ -410,7 +432,7 @@ class VSETB_OT_upload_to_tracker(Operator): comment=comment_data, 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) params = {} @@ -434,7 +456,9 @@ class VSETB_OT_upload_to_tracker(Operator): task = getattr(strip_settings.tasks, norm_name(task_type.name)) tracker_task = tracker.get_task(task_type.name, entity=shot) - if task.comment and tracker_task.get('last_comment') != task.comment: + 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 task.comment and last_comment_text != task.comment: tracker.new_comment(tracker_task, comment=task.comment) context.window_manager.progress_end() diff --git a/resources/trackers/kitsu.py b/resources/trackers/kitsu.py index 15af0df..0b8bea8 100644 --- a/resources/trackers/kitsu.py +++ b/resources/trackers/kitsu.py @@ -15,7 +15,7 @@ from vse_toolbox.file_utils import install_module, norm_str from vse_toolbox.resources.trackers.tracker import Tracker try: - gazu = install_module('gazu') + gazu = install_module('gazu', package_name='gazu>=1.0,<2.0') except Exception as e: print('Could not install gazu') print(e) @@ -32,10 +32,15 @@ class Kitsu(Tracker): admin_password : StringProperty(subtype='PASSWORD') 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'): url += '/api' + gazu.client.set_host(url) + login = expandvars(self.admin_login or self.login) password = expandvars(self.admin_password or self.password) @@ -347,6 +352,26 @@ class Kitsu(Tracker): status = self.get_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): project = self.get_project(project) diff --git a/scene_cut_detection.py b/scene_cut_detection.py index d042182..c038a26 100644 --- a/scene_cut_detection.py +++ b/scene_cut_detection.py @@ -11,7 +11,7 @@ def detect_scene_change(strip, movie_type="ANIMATED", threshold=0.5, frame_start """Launch ffmpeg command to detect changing frames from a movie strip. Args: - movie_strip (bpy.types.Sequence): blender sequence strip to detect changes. + 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. diff --git a/sequencer_utils.py b/sequencer_utils.py index b40592f..ae60b46 100644 --- a/sequencer_utils.py +++ b/sequencer_utils.py @@ -41,17 +41,17 @@ def frame_to_timecode(frame, fps): return timecode 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): - sequences = bpy.context.scene.sequence_editor.sequences - strip = sequences.new_effect(name, 'TEXT', channel, frame_start=start, frame_end=end) + strips = bpy.context.scene.sequence_editor.strips + strip = strips.new_effect(name, 'TEXT', channel, frame_start=start, length=end - start) strip.select = select strip.text = text strip.location.x = x strip.location.y = y - strip.align_y = align_y - strip.align_x = align_x + strip.anchor_y = anchor_y + strip.anchor_x = anchor_x strip.channel = channel strip.font_size = font_size @@ -74,7 +74,7 @@ def get_strips(channel=0, selected_only=False): if isinstance(channel, str): 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: strips = [s for s in strips if s.select] @@ -141,7 +141,7 @@ def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_ name = template.format(**format_data) - existing_strip = scn.sequence_editor.sequences_all.get(name) + existing_strip = scn.sequence_editor.strips_all.get(name) if existing_strip: existing_strip.name = f"{name}_tmp" @@ -331,7 +331,7 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'): from opentimelineio.schema import Clip scn = bpy.context.scene - sequencer = scn.sequence_editor.sequences + sequencer = scn.sequence_editor.strips strips = get_strips(channel='Shots') shot_channel = get_channel_index('Shots') @@ -341,18 +341,26 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'): s.channel = empty_channel 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: timeline = opentimelineio.adapters.read_from_file( - str(edl), adapter, rate=scn.render.fps, ignore_timecode_mismatch=True) + str(edl), adapter, **extra_kwargs) except: print("[>.] read_from_file Failed. Using read_from_string method.") data = edl.read_text(encoding='latin-1') timeline = opentimelineio.adapters.read_from_string( - data, adapter, rate=scn.render.fps, ignore_timecode_mismatch=True) + data, adapter, **extra_kwargs) - scn.frame_start = ( - 0 if timeline.global_start_time is None else timeline.global_start_time - ) + global_start = 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 clips = [] @@ -411,7 +419,7 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'): type='COLOR', channel=shot_channel, frame_start=frame_start, - frame_end=frame_end, + length=frame_end - frame_start, ) strip.blend_alpha = 0.0 @@ -439,7 +447,7 @@ def import_movie(filepath, frame_start=None, frame_end=None): if len(relpath.as_posix()) < len(filepath.as_posix()): filepath = relpath - strip = scn.sequence_editor.sequences.new_movie( + strip = scn.sequence_editor.strips.new_movie( name=Path(filepath).stem, filepath=str(filepath), channel=get_channel_index('Movie'), @@ -480,7 +488,7 @@ def import_sound(filepath, frame_start=None, frame_end=None): if len(relpath.as_posix()) < len(filepath.as_posix()): filepath = relpath - strip = scn.sequence_editor.sequences.new_sound( + strip = scn.sequence_editor.strips.new_sound( name=f'{filepath.stem} Audio', filepath=str(filepath), channel=get_channel_index('Audio'), @@ -495,7 +503,7 @@ def import_sound(filepath, frame_start=None, frame_end=None): return strip 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): scn = bpy.context.scene @@ -509,7 +517,7 @@ def clean_sequencer(edit=False, movie=False, sound=False): sequences.extend(get_strips('Audio')) for sequence in sequences: - scn.sequence_editor.sequences.remove(sequence) + scn.sequence_editor.strips.remove(sequence) @persistent def set_active_strip(scene): @@ -531,7 +539,7 @@ def set_active_strip(scene): @persistent 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 #print("update_text_strips") @@ -577,7 +585,7 @@ def update_text_strips(scene): }) 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': continue @@ -593,12 +601,12 @@ def update_text_strips(scene): 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, 'COLOR', get_channel_index(channel), frame_start=start, - frame_end=end + length=end - start ) shot_strip.blend_alpha = 0 shot_strip.color = (0.5, 0.5, 0.5) diff --git a/ui/panels.py b/ui/panels.py index 037e8ae..ffc3601 100644 --- a/ui/panels.py +++ b/ui/panels.py @@ -139,7 +139,7 @@ class VSETB_PT_strip(Panel): @classmethod def poll(cls, context): - strip = context.active_sequence_strip + strip = context.active_strip return strip and get_channel_name(strip) == 'Shots' def draw(self, context): @@ -174,7 +174,7 @@ class VSETB_PT_sequencer(VSETB_main, Panel): settings = get_scene_settings() project = settings.active_project - strip = context.active_sequence_strip + strip = context.active_strip channel = get_channel_name(strip) col = layout.column() @@ -224,6 +224,7 @@ class VSETB_PT_imports(VSETB_main, Panel): #col.operator('vse_toolbox.import_files', text='Import', icon='IMPORT') col.operator('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): @@ -264,10 +265,10 @@ class VSETB_PT_tracker(VSETB_main, Panel): @classmethod def poll(cls, context): - return context.active_sequence_strip + return context.active_strip def draw_header_preset(self, context): - active_strip = context.active_sequence_strip + active_strip = context.active_strip self.layout.label(text=active_strip.name) def draw(self, context): @@ -280,7 +281,7 @@ class VSETB_PT_casting(VSETB_main, Panel): @classmethod def poll(cls, context): - strip = context.active_sequence_strip + strip = context.active_strip return strip and get_channel_name(strip) == 'Shots' def draw(self, context): @@ -315,6 +316,8 @@ class VSETB_PT_casting(VSETB_main, Panel): col_tool.separator() 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: casting_item = strip_settings.casting[strip_settings.casting_index] @@ -336,7 +339,7 @@ class VSETB_PT_metadata(VSETB_main, Panel): @classmethod 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): @@ -383,7 +386,7 @@ class VSETB_PT_comments(VSETB_main, Panel): @classmethod 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): layout = self.layout @@ -455,6 +458,7 @@ class VSETB_MT_main_menu(Menu): 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): diff --git a/ui/preferences.py b/ui/preferences.py index 30252b3..810a1b0 100644 --- a/ui/preferences.py +++ b/ui/preferences.py @@ -91,7 +91,11 @@ def load_prefs(): continue 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() class Trackers(PropertyGroup): @@ -155,7 +159,7 @@ def register(): config_path = os.getenv('VSE_TOOLBOX_CONFIG') if config_path: - prefs['config_path'] = os.path.expandvars(config_path) + prefs.config_path = os.path.expandvars(config_path) load_prefs() diff --git a/ui/properties.py b/ui/properties.py index 88bbe6a..62316e2 100644 --- a/ui/properties.py +++ b/ui/properties.py @@ -299,7 +299,28 @@ def get_episodes_items(self, context): def on_episode_updated(self, context): 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): id : StringProperty(default='') @@ -715,7 +736,7 @@ class VSETB_PGT_strip_settings(PropertyGroup): @property 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) @property @@ -808,7 +829,7 @@ def register(): bpy.utils.register_class(cls) 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() bpy.app.handlers.load_post.append(load_handler) @@ -818,7 +839,7 @@ def unregister(): for cls in reversed(classes): 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 bpy.app.handlers.load_post.remove(load_handler)