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

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

871 lines
29 KiB
Python

from pathlib import Path
from os.path import expandvars
import re
import glob
from tempfile import gettempdir
import bpy
from bpy.types import Operator, UIList
from bpy.props import (CollectionProperty, BoolProperty, EnumProperty, StringProperty, IntProperty)
from vse_toolbox.constants import (EDITS, EDIT_SUFFIXES, MOVIES, MOVIE_SUFFIXES,
SOUNDS, SOUND_SUFFIXES)
from vse_toolbox.sequencer_utils import (clean_sequencer, import_edit, import_movie,
import_sound, get_strips, get_channel_index, get_empty_channel, scale_clip_to_fit)
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
class VSETB_OT_auto_select_files(Operator):
bl_idname = "vse_toolbox.auto_select_files"
bl_label = "Auto Select"
bl_description = "Auto Select Files"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return True
def get_items(self, items=[]):
if not items:
return [('NONE', 'None', '', 0)]
return [(e, e, '', i) for i, e in enumerate(items)]
def execute(self, context):
params = context.space_data.params
directory = Path(params.directory.decode())
EDITS.clear()
MOVIES.clear()
SOUNDS.clear()
edits = []
movies = []
sounds = []
for file_entry in directory.glob('*'):
if file_entry.is_dir():
continue
if file_entry.suffix in EDIT_SUFFIXES:
edits.append(file_entry.name)
elif file_entry.suffix in MOVIE_SUFFIXES:
movies.append(file_entry.name)
elif file_entry.suffix in SOUND_SUFFIXES:
sounds.append(file_entry.name)
edits.sort(reverse=True)
movies.sort(reverse=True)
sounds.sort(reverse=True)
EDITS.extend(self.get_items(items=edits))
MOVIES.extend(self.get_items(items=movies))
SOUNDS.extend(self.get_items(items=sounds))
return {'FINISHED'}
class VSETB_OT_import_files(Operator):
bl_idname = "vse_toolbox.import_files"
bl_label = "Import"
bl_description = "Import Edit"
bl_options = {"REGISTER", "UNDO"}
directory : StringProperty(subtype='DIR_PATH')
filepath: StringProperty(
name="File Path",
description="Filepath used for importing the file",
maxlen=1024,
subtype='FILE_PATH',
)
files : CollectionProperty(type=bpy.types.OperatorFileListElement)
clean_sequencer : BoolProperty(
name="Clean Sequencer",
default=False,
description="Clean all existing strips in sequencer",
)
import_edit : BoolProperty(name='', default=True)
edit: EnumProperty(name='', items=lambda s, c: EDITS)
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)
movie: EnumProperty(name='', items=lambda s, c: MOVIES)
import_sound : BoolProperty(name='', default=False)
sound: EnumProperty(name='', items=lambda s, c: SOUNDS)
@classmethod
def poll(cls, context):
return True
def draw(self, context):
scn = context.scene
settings = get_scene_settings()
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
col = layout.column(align=True)
col.operator('vse_toolbox.auto_select_files', text='Auto Select')
row = layout.row(heading="Import Edit", align=True)
row.prop(self, 'import_edit')
sub = row.row(align=True)
sub.active = self.import_edit
sub.prop(self, 'edit')
row = 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()
row = layout.row(heading="Import Movie", align=True)
row.prop(self, 'import_movie')
sub = row.row()
sub.active = self.import_movie
sub.prop(self, 'movie')
row = layout.row(heading="Import Sound", align=True)
row.prop(self, 'import_sound')
sub = row.row()
sub.active = self.import_sound
sub.prop(self, 'sound')
col = layout.column()
col.separator()
col.prop(self, 'clean_sequencer')
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
sequencer = context.scene.sequence_editor.strips
edit_filepath = Path(self.directory, self.edit)
if not edit_filepath.exists():
self.import_edit = False
movie_filepath = Path(self.directory, self.movie)
if not movie_filepath.exists():
self.import_movie = False
sound_filepath = Path(self.directory, self.sound)
if not sound_filepath.exists():
self.import_sound = False
if self.clean_sequencer:
clean_sequencer(
edit=self.import_edit,
movie=self.import_movie,
sound=self.import_sound,
)
if self.import_edit:
print(f'[>.] Loading Edit from: {str(edit_filepath)}')
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)}')
for strip in get_strips(channel='Movie'):
sequencer.remove(strip)
import_movie(movie_filepath)
if self.import_sound or (not self.import_sound and self.import_movie):
for strip in get_strips(channel='Audio'):
sequencer.remove(strip)
if self.import_sound:
print(f'[>.] Loading Audio from: {str(sound_filepath)}')
import_sound(sound_filepath)
else:
print(f'[>.] Loading Audio from: {str(movie_filepath)}')
import_sound(movie_filepath)
context.scene.sequence_editor.strips.update()
return {"FINISHED"}
class VSETB_UL_import_task(UIList):
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
layout.separator(factor=0.5)
layout.prop(item, 'import_enabled', text='')
layout.label(text=item.name)
def get_task_items(self, context):
settings = get_scene_settings()
project = settings.active_project
return [(t, t, '') for t in project.task_types.keys()]
class VSETB_OT_select_task(Operator):
bl_idname = "vse_toolbox.select_task"
bl_label = "Select Task"
bl_description = "Select Task"
bl_options = {"REGISTER", "UNDO"}
bl_property = "task"
task: EnumProperty(name="My Search", items=get_task_items)
@classmethod
def poll(cls, context):
return True
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.task_types[self.task].import_enabled = True
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class VSETB_OT_unselect_task(Operator):
bl_idname = "vse_toolbox.unselect_task"
bl_label = "Unselect Task"
bl_description = "Unselect Task"
bl_options = {"REGISTER", "UNDO"}
task : StringProperty()
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.task_types[self.task].import_enabled = False
return {'FINISHED'}
class VSETB_OT_add_import_template(Operator):
bl_idname = "vse_toolbox.add_import_template"
bl_label = "Add Template"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.import_shots.video_templates.add()
return {'FINISHED'}
class VSETB_OT_remove_import_template(Operator):
bl_idname = "vse_toolbox.remove_import_template"
bl_label = "Remove Template"
bl_options = {"REGISTER", "UNDO"}
index : IntProperty()
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.import_shots.video_templates.remove(self.index)
return {'FINISHED'}
def get_sequence_items(self, context):
settings = get_scene_settings()
project = settings.active_project
return [(t, t, '') for t in project.sequences.keys()]
class VSETB_OT_select_sequence(Operator):
bl_idname = "vse_toolbox.select_sequence"
bl_label = "Select Sequence"
bl_description = "Select Sequence"
bl_options = {"REGISTER", "UNDO"}
bl_property = "sequence"
sequence: EnumProperty(name="My Search", items=get_sequence_items)
@classmethod
def poll(cls, context):
return True
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.sequences[self.sequence].import_enabled = True
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class VSETB_OT_unselect_sequence(Operator):
bl_idname = "vse_toolbox.unselect_sequence"
bl_label = "Unselect sequence"
bl_description = "Unselect sequence"
bl_options = {"REGISTER", "UNDO"}
sequence : StringProperty()
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.sequences[self.sequence].import_enabled = False
return {'FINISHED'}
class VSETB_OT_import_shots(Operator):
bl_idname = "vse_toolbox.import_shots"
bl_label = "Import Shots"
bl_description = "Import Shots for disk or tracker"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return get_scene_settings().active_project
def set_sequencer_channels(self, task_types):
scn = bpy.context.scene
channels = ['Shots', 'Sequences', 'Stamps']
#channel_index = len(task_types * 3) + len(channels) + 1
channel_index = 1
for task_type in task_types:
audio_channel = scn.sequence_editor.channels[channel_index]
audio_channel.name = f'{task_type} Audio'
audio_channel.mute = (task_type != task_types[-1])
video_channel = scn.sequence_editor.channels[channel_index+1]
video_channel.name = f'{task_type} Video'
channel_index += 3
for channel in channels:
scn.sequence_editor.channels[channel_index].name = channel
channel_index += 1
# def get_preview_dir(self):
# preview_dir = Path(bpy.app.tempdir, 'previews')
# if bpy.data.filepath:
# preview_dir = Path(bpy.data.filepath).parent / 'previews'
# return preview_dir
def download_preview(self, task_type, shot):
prefs = get_addon_prefs()
tracker = prefs.tracker
settings = get_scene_settings()
project = settings.active_project
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)
if not last_comment:
return
last_preview = last_comment['previews'][0]
ext = last_preview['extension']
shot_name = shot['name']
sequence_name = f"{shot['sequence_name']}_"
if shot_name.startswith(sequence_name):
shot_name = shot_name.replace(sequence_name, '', 1)
preview_dir = abspath(project.import_shots.previews_folder)#self.get_preview_dir()
filepath = Path(preview_dir, f'{sequence_name}{shot_name}_{task_type.name}.{ext}')
filepath.parent.mkdir(parents=True, exist_ok=True)
tracker.download_preview_file(last_preview, str(filepath))
return filepath
def conform_render(self, clip):
scn = bpy.context.scene
scn.render.resolution_x = clip.elements[0].orig_width
scn.render.resolution_y = clip.elements[0].orig_height
scn.render.fps = int(clip.elements[0].orig_fps)
scn.view_settings.view_transform = 'Standard'
def find_shot_preview(self, sequence, shot_name, task_type):
settings = get_scene_settings()
project = settings.active_project
import_shots = project.import_shots
project_templates = {t.name: t.value for t in project.templates}
for template in [import_shots.video_template] + list(import_shots.video_templates.keys()):
template = expand(template, **project_templates)
format_data = project.format_data
format_data.update(parse(project.sequence_template, sequence.name))
format_data.update(parse(project.shot_template, shot_name))
# Normalize name in snake_case for now
format_data.update(task=task_type.name.lower().replace(' ', '_'))
if last_preview := find_last(template, **format_data):
return last_preview
def import_casting(self, shot, strip):
prefs = get_addon_prefs()
tracker = prefs.tracker
strip_settings = strip.vsetb_strip_settings
casting_data = tracker.get_casting(shot)
for asset_data in casting_data:
item = strip_settings.casting.add()
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()
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
import_shots = project.import_shots
prefs = get_addon_prefs()
tracker = prefs.tracker
tracker.admin_connect()
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:
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])
for sequence in sequences:
shots_data = tracker.get_shots(sequence=sequence.id)
sequence_start = frame_index
for shot_data in shots_data:
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
strip = scn.sequence_editor.strips.new_effect(
name=shot_data['name'],
type='COLOR',
channel=get_channel_index('Shots'),
frame_start=frame_index,
length=frames
)
strip.blend_alpha = 0
strip.color = (0.5, 0.5, 0.5)
self.import_casting(shot_data, strip)
for task_type in task_types:
if import_shots.import_source == 'DISK':
preview = self.find_shot_preview(sequence, shot_data['name'], task_type)
else:
preview = self.download_preview(task_type, shot_data)
if not preview:
print(f'No preview found for shot {shot_data["name"]}')
if not preview:
continue
print(f'Loading Preview from {preview}')
channel_index = get_channel_index(f'{task_type.name} Video')
video_clip = import_movie(preview, frame_start=frame_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:
video_clip.color_tag = 'COLOR_01'
if not conformed:
self.conform_render(video_clip)
conformed = True
scale_clip_to_fit(video_clip)
# Load Audio
channel_index = get_channel_index(f'{task_type.name} Audio')
audio_clip = import_sound(preview, frame_start=frame_index)
audio_clip.channel = channel_index
if video_clip.frame_offset_end:
audio_clip.color_tag = 'COLOR_01'
frame_index += frames
strip = scn.sequence_editor.strips.new_effect(
name=sequence.name,
type='COLOR',
channel=get_channel_index('Sequences'),
frame_start=sequence_start,
length=frame_index - sequence_start
)
strip.blend_alpha = 0
strip.color = (0.25, 0.25, 0.25)
scn.frame_start = 1
scn.frame_end = frame_index -1
#bpy.ops.vse_toolbox.set_stamps()
return {'FINISHED'}
def draw(self, context):
settings = get_scene_settings()
project = settings.active_project
import_shots = project.import_shots
layout = self.layout
col = layout.column(align=False)
col.use_property_split = True
col.use_property_decorate = False
row = col.row(align=True)
row.prop(import_shots, "import_source", text='Videos Source', expand=True)
if import_shots.import_source == 'DISK':
#col.prop(import_shots, "sequence_dir_template", text='Sequence Dir')
#col.prop(import_shots, "shot_folder_template", text='Shot Folder')
row = col.row(align=True)
row.prop(import_shots, "video_template", text='')
row.operator("vse_toolbox.add_import_template", text='', icon='ADD')
for i, template in enumerate(import_shots.video_templates):
row = col.row(align=True)
row.prop(template, "name", text='')
row.operator("vse_toolbox.remove_import_template", text='', icon='REMOVE').index=i
col.separator()
else:
col.prop(import_shots, "previews_folder", text='Previews Folder')
col.separator()
#if bpy.data.filepath:
#col.label(text=f' {self.get_preview_dir()}')
#else:
# col.label(icon="ERROR", text="Save your Blender to keep the previews")
row = col.row(align=True)
row.prop(import_shots, 'import_task', text='Import Tasks')
row.operator("vse_toolbox.select_task", text='', icon='ADD')
tasks = [t for t in project.task_types if t.import_enabled]
if import_shots.import_task == "FROM_LIST":
if tasks:
split = col.split(factor=0.4)
split.row()
box = split.box()
box_col = box.column(align=True)
for task_type in tasks:
row = box_col.row(align=True)
sub = row.row(align=True)
sub.enabled = False
sub.alignment = 'LEFT'
sub.scale_x = 0.15
sub.prop(task_type, 'color', text='')
op = row.operator("vse_toolbox.unselect_task", text=task_type.name)
op.task = task_type.name
row.separator(factor=0.5)
op = row.operator("vse_toolbox.unselect_task", text='', icon='REMOVE', emboss=False)
op.task = task_type.name
else:
split = col.split(factor=0.4)
split.row()
row = split.row()
row.label(icon="ERROR")
row.label(text='Add at least one task')
# Choose Sequences
row = col.row(align=True)
row.prop(import_shots, 'import_sequence', text='Import Sequences')
row.operator("vse_toolbox.select_sequence", text='', icon='ADD')
sequences = [s for s in project.sequences if s.import_enabled]
if import_shots.import_sequence == "FROM_LIST":
if sequences:
split = col.split(factor=0.4)
split.row()
box = split.box()
box_col = box.column(align=True)
for sequence in sequences:
row = box_col.row(align=True)
op = row.operator("vse_toolbox.unselect_sequence", text=sequence.name)
op.sequence = sequence.name
row.separator(factor=0.5)
op = row.operator("vse_toolbox.unselect_sequence", text='', icon='REMOVE', emboss=False)
op.sequence = sequence.name
else:
split = col.split(factor=0.4)
split.row()
row = split.row()
row.label(icon="ERROR")
row.label(text='Add at least one Sequence')
layout.prop(import_shots, 'clear', text='Clear')
def invoke(self, context, event):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
tmp_dir = Path(gettempdir(), 'reviews')
if bpy.data.filepath and Path(project.import_shots.previews_folder) == tmp_dir:
project.import_shots.previews_folder = '//sources'
if not bpy.data.filepath and project.import_shots.previews_folder == '//sources':
project.import_shots.previews_folder = str(tmp_dir)
# if not bpy.data.filepath:
# self.report({"ERROR"}, "Save your Blender file first")
# return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self, width=350)
def check(self, context):
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 = (
VSETB_OT_select_sequence,
VSETB_OT_unselect_sequence,
VSETB_OT_unselect_task,
VSETB_OT_select_task,
VSETB_OT_add_import_template,
VSETB_OT_remove_import_template,
VSETB_UL_import_task,
VSETB_OT_auto_select_files,
VSETB_OT_import_files,
VSETB_OT_import_stb_xml,
VSETB_OT_import_shots,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)