Compare commits

..
2 Commits
Author SHA1 Message Date
florentin.luce e50a7aee31 fix shot_template 2024-04-19 12:18:56 +02:00
florentin.luce 948f5b53dd fix stamps 2024-04-18 18:02:38 +02:00
18 changed files with 320 additions and 1556 deletions
+6 -12
View File
@@ -36,34 +36,28 @@ import bpy
def register():
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)
bpy.app.handlers.frame_change_post.append(update_text_strips)
bpy.app.handlers.render_pre.append(update_text_strips)
for module in modules:
module.register()
if set_active_strip not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(set_active_strip)
bpy.app.handlers.frame_change_post.append(set_active_strip)
prefs = get_addon_prefs()
#print('\n\n-------------------', prefs.config_path)
def unregister():
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)
bpy.app.handlers.frame_change_post.remove(update_text_strips)
bpy.app.handlers.render_pre.remove(update_text_strips)
try:
bpy.utils.previews.remove(ASSET_PREVIEWS)
except Exception as e:
pass
if set_active_strip in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(set_active_strip)
bpy.app.handlers.frame_change_post.remove(set_active_strip)
for module in reversed(modules):
module.unregister()
+2 -1
View File
@@ -49,7 +49,8 @@ def get_scene_settings():
return bpy.context.scene.vsetb_settings
def get_strip_settings():
strip = bpy.context.active_strip
scn = bpy.context.scene
strip = bpy.context.active_sequence_strip
if not strip:
return
+10 -11
View File
@@ -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(r'[ ]+/[ ]+', '/', string)
string = re.sub('[ ]+\/[ ]+', '/', 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(r'[ ]+/[ ]+', '/', string)
string = re.sub('[ ]+\/[ ]+', '/', string)
string = string.strip()
if format:
@@ -89,9 +89,9 @@ def norm_name(string, separator='_', format=str.lower, padding=0):
return string
def read_file(path):
'''Read a file with an extension in (json, yaml, yml, txt, log)'''
'''Read a file with an extension in (json, yaml, yml, txt)'''
exts = ('.json', '.yaml', '.yml', '.txt', '.log')
exts = ('.json', '.yaml', '.yml', '.txt')
if not path:
print('Try to read empty file')
@@ -147,20 +147,19 @@ def open_file(filepath, env=None, select=False):
subprocess.Popen(cmd, env=env)
# def parse(string, template):
# template = re.sub(r'{index:(.+?)}', r'(?P<index>.+)', template)
# reg = re.sub(r'{(.+?)}', r'(?P<_\1>.+)', template)
"""def parse(template, string):
template = re.sub(r'{index:(.+?)}', r'(?P<index>.+)', template)
reg = re.sub(r'{(.+?)}', r'(?P<_\1>.+)', template)
# values = list(re.search(reg, string).groups())
# keys = re.findall(r'{(.+?)}', template) + ['index']
values = list(re.search(reg, string).groups())
keys = re.findall(r'{(.+?)}', template) + ['index']
# return dict(zip(keys, values))
return dict(zip(keys, values))"""
def parse(template, string):
reg = re.sub(r'{[^}]*}|\.', lambda m: m.group() if m.group().startswith('{') else r'\.', template)
reg = re.sub(r'{(.+?)}', r'(?P<\1>.*)', reg)
#print(string, template, reg)
if result := re.match(reg, string):
return result.groupdict()
-3
View File
@@ -204,9 +204,6 @@ class VSETB_OT_remove_template(Operator):
return {'FINISHED'}
classes = (
VSETB_OT_reload_addon,
VSETB_OT_load_settings,
+17 -77
View File
@@ -53,7 +53,7 @@ class VSETB_OT_casting_replace(Operator):
item = self.assets.add()
item.name = asset.tracker_name
strip = context.active_strip
strip = context.active_sequence_strip
asset_casting_index = strip.vsetb_strip_settings.casting_index
active_asset = strip.vsetb_strip_settings.casting[asset_casting_index].asset
@@ -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_strip
active_strip = context.scene.sequence_editor.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_strip
active_strip = context.scene.sequence_editor.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_strip
active_strip = context.scene.sequence_editor.active_strip
if active_strip:
return True
@@ -229,15 +229,16 @@ class VSETB_OT_copy_casting(Operator):
@classmethod
def poll(cls, context):
if not context.scene.sequence_editor:
return
active_strip = context.active_strip
active_strip = context.scene.sequence_editor.active_strip
strip_settings = get_strip_settings()
if active_strip and strip_settings.casting:
return True
def execute(self, context):
scn = context.scene
active_strip = scn.sequence_editor.active_strip
strip_settings = get_strip_settings()
datas = [c.to_dict() for c in strip_settings.casting]
@@ -256,7 +257,9 @@ class VSETB_OT_paste_casting(Operator):
@classmethod
def poll(cls, context):
return context.active_strip
active_strip = context.scene.sequence_editor.active_strip
if active_strip:
return True
def invoke(self, context, event):
self.mode = 'REPLACE'
@@ -279,7 +282,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_strips:
for strip in context.selected_sequences:
strip_settings = strip.vsetb_strip_settings
if self.mode == 'REPLACE':
@@ -317,7 +320,7 @@ class VSETB_OT_copy_metadata(Operator):
@classmethod
def poll(cls, context):
return context.selected_strips and context.active_strip
return context.selected_sequences and context.active_sequence_strip
def execute(self, context):
prefs = get_addon_prefs()
@@ -329,11 +332,11 @@ class VSETB_OT_copy_metadata(Operator):
if not metadata:
self.report({'ERROR'}, f'No Metadata named {self.metadata}')
active_strip = context.active_strip
active_strip = context.active_sequence_strip
metadata_value = getattr(active_strip.vsetb_strip_settings.metadata, metadata)
for strip in context.selected_strips:
if strip == context.active_strip:
for strip in context.selected_sequences:
if strip == context.active_sequence_strip:
continue
setattr(strip.vsetb_strip_settings.metadata, metadata, metadata_value)
@@ -341,68 +344,6 @@ 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,
@@ -410,8 +351,7 @@ classes = (
VSETB_OT_copy_casting,
VSETB_OT_paste_casting,
VSETB_OT_casting_replace,
VSETB_OT_copy_metadata,
VSETB_OT_casting_create_asset,
VSETB_OT_copy_metadata
)
def register():
-1
View File
@@ -217,7 +217,6 @@ class VSETB_OT_export_edl(Operator):
def execute(self, context):
opentimelineio = install_module('opentimelineio')
install_module('opentimelineio', package_name='opentimelineio-plugins')
from opentimelineio.schema import (Clip, Timeline, Track, ExternalReference)
from opentimelineio.opentime import (RationalTime, TimeRange)
+15 -225
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,
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.bl_utils import (get_scene_settings, get_addon_prefs, get_scene_settings, abspath)
from vse_toolbox.file_utils import install_module, parse, find_last, expand
@@ -90,15 +90,6 @@ 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)
@@ -128,8 +119,6 @@ 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()
@@ -155,7 +144,7 @@ class VSETB_OT_import_files(Operator):
return {'RUNNING_MODAL'}
def execute(self, context):
sequencer = context.scene.sequence_editor.strips
sequencer = context.scene.sequence_editor.sequences
edit_filepath = Path(self.directory, self.edit)
if not edit_filepath.exists():
@@ -179,12 +168,7 @@ class VSETB_OT_import_files(Operator):
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)
import_edit(edit_filepath, adapter="cmx_3600", match_by=self.match_by)
if self.import_movie:
print(f'[>.] Loading Movie from: {str(movie_filepath)}')
@@ -206,7 +190,7 @@ class VSETB_OT_import_files(Operator):
print(f'[>.] Loading Audio from: {str(movie_filepath)}')
import_sound(movie_filepath)
context.scene.sequence_editor.strips.update()
context.scene.sequence_editor.sequences.update()
return {"FINISHED"}
@@ -398,9 +382,6 @@ class VSETB_OT_import_shots(Operator):
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
@@ -449,22 +430,6 @@ class VSETB_OT_import_shots(Operator):
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()
@@ -478,45 +443,30 @@ 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:
frame_index = 1
scn.sequence_editor_clear()
scn.sequence_editor_create()
else:
frame_index = scn.frame_end +1
scn.sequence_editor_clear()
scn.sequence_editor_create()
self.set_sequencer_channels([t.name for t in task_types])
frame_index = 1
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)
frames = int(shot_data['nb_frames'])
frame_end = frame_index + frames
strip = scn.sequence_editor.strips.new_effect(
strip = scn.sequence_editor.sequences.new_effect(
name=shot_data['name'],
type='COLOR',
channel=get_channel_index('Shots'),
frame_start=frame_index,
length=frames
frame_end=frame_index + 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)
@@ -530,13 +480,9 @@ class VSETB_OT_import_shots(Operator):
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 = import_movie(preview, frame_start=frame_index, frame_end=frame_end)
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'
@@ -548,19 +494,20 @@ class VSETB_OT_import_shots(Operator):
# Load 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
if video_clip.frame_offset_end:
audio_clip.color_tag = 'COLOR_01'
frame_index += frames
strip = scn.sequence_editor.strips.new_effect(
strip = scn.sequence_editor.sequences.new_effect(
name=sequence.name,
type='COLOR',
channel=get_channel_index('Sequences'),
frame_start=sequence_start,
length=frame_index - sequence_start
frame_end=frame_index
)
strip.blend_alpha = 0
strip.color = (0.25, 0.25, 0.25)
@@ -669,8 +616,6 @@ class VSETB_OT_import_shots(Operator):
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()
@@ -694,160 +639,6 @@ 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 <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,
@@ -858,7 +649,6 @@ classes = (
VSETB_UL_import_task,
VSETB_OT_auto_select_files,
VSETB_OT_import_files,
VSETB_OT_import_stb_xml,
VSETB_OT_import_shots,
)
+58 -537
View File
@@ -1,18 +1,14 @@
import re
from os.path import expandvars, abspath
from pathlib import Path
import bpy
from bpy.types import Operator
from bpy.props import (BoolProperty, StringProperty, FloatProperty,
IntProperty, EnumProperty)
from bpy.props import (BoolProperty, StringProperty)
from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels,
get_channel_index, new_text_strip, get_strip_at, get_channel_name,
create_shot_strip, update_text_strips)
get_channel_index, new_text_strip, get_strip_at, get_channel_name)
from vse_toolbox.scene_cut_detection import detect_scene_change
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings, get_addon_prefs
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings
from vse_toolbox.constants import REVIEW_TEMPLATE_BLEND
from shutil import copy2
@@ -24,11 +20,7 @@ class VSETB_OT_rename(Operator):
#template : StringProperty(name="Strip Name", default="")
#increment : IntProperty(name="Increment", default=0)
selected_only : BoolProperty(
name="Selected Only",
default=False,
description="Rename only selected strips; disabled renames the whole channel",
)
selected_only : BoolProperty(name="Selected Only", default=True)
#start_number : IntProperty(name="Start Number", default=0, min=0)
#by_sequence : BoolProperty(
# name="Reset By Sequence",
@@ -38,13 +30,9 @@ class VSETB_OT_rename(Operator):
@classmethod
def poll(cls, context):
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,
)
return strip is not None
settings = get_scene_settings()
strip = context.active_sequence_strip
return settings.active_project and get_channel_name(strip) in ('Shots', 'Sequences')
def invoke(self, context, event):
scn = context.scene
@@ -57,29 +45,19 @@ class VSETB_OT_rename(Operator):
scn = context.scene
settings = get_scene_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
episode = project.episode_name
sequence = str(project.sequence_start_number).zfill(project.sequence_padding)
shot = str(project.shot_start_number).zfill(project.shot_padding)
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,
)
strip = context.active_sequence_strip
channel_name = get_channel_name(strip)
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
if channel_name != 'Sequences':
if channel_name == 'Shots':
col.prop(project, 'shot_template', text='Shot Name')
col.prop(project, 'shot_start_number', text='Start Number')
col.prop(project, 'shot_increment', text='Increment')
@@ -95,9 +73,9 @@ class VSETB_OT_rename(Operator):
col.prop(self, 'selected_only')
if channel_name != 'Sequences':
if channel_name == 'Shots':
label = project.shot_template.format(episode=episode, sequence=sequence, shot=shot)
else:
elif channel_name == 'Sequences':
label = project.sequence_template.format(episode=episode, sequence=sequence)
col.label(text=f'Renaming {label}')
@@ -106,30 +84,12 @@ class VSETB_OT_rename(Operator):
scn = context.scene
settings = get_scene_settings()
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_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,
)
strip = context.active_sequence_strip
channel_name = get_channel_name(strip)
strips = get_strips(channel=channel_name, selected_only=self.selected_only)
if channel_name != 'Sequences':
if channel_name == 'Shots':
rename_strips(strips,
template=project.shot_template,
increment=project.shot_increment, start_number=project.shot_start_number,
@@ -188,13 +148,8 @@ class VSETB_OT_set_sequencer(Operator):
movie = movies[0]
movie.transform.scale_x = movie.transform.scale_y = 1
elem = movie.strip_elem_from_frame(scn.frame_current)
if elem is None and movie.elements:
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.')
scn.render.resolution_x = elem.orig_width
scn.render.resolution_y = elem.orig_height
else:
self.report({'INFO'}, f'Cannot set Resolution. No Movie Found.')
@@ -217,160 +172,6 @@ class VSETB_OT_set_sequencer(Operator):
return {"FINISHED"}
class VSETB_OT_scene_cut_detection(Operator):
"""Launch subprocess with ffmpeg and python to find and create each
shots strips from video source"""
bl_idname = "vse_toolbox.scene_cut_detection"
bl_label = "Scene Cut Detection"
bl_description = "Detect scene change and create strips on the destination channel, use crop to restric detection"
bl_options = {"REGISTER", "UNDO"}
animated_threshold: FloatProperty(name="Threshold", default=0.5, min=0, max=1,
description='Probability for the current frame to introduce a new scene')
still_threshold: FloatProperty(name="Threshold", default=0.001, min=0, max=1, precision=4,
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),
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):
sequencer = context.scene.sequence_editor
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]
# Prefer the channel containing the selected strip. With no selected
# strip, use the first available channel (channel 0 in the UI list).
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)
def draw(self, context):
layout = self.layout
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
row = col.row(align=True)
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.prop(self, 'frame_start', text='Frame Split First')
split_col.prop(self, 'frame_end', text='Last')
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)
return {'PASS_THROUGH'}
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.source_channel_name)
i = 1
frame_start = self.frame_start
for strip in strips:
if strip.type != 'MOVIE':
continue
# Skip strip outside the frame range to create shot from.
if strip.frame_final_start >= self.frame_end or strip.frame_final_end <= self.frame_start:
continue
threshold = self.animated_threshold if self.movie_type == 'ANIMATED' else self.still_threshold
params = dict(strip=strip, movie_type= self.movie_type, threshold=threshold,
frame_start=self.frame_start, frame_end=self.frame_end,
crop=(strip.crop.min_x, strip.crop.max_x, strip.crop.min_y, strip.crop.max_y))
for frame_end in detect_scene_change(**params):
if not frame_end:
continue
# Convert movie frame to strips frame
if frame_start+int(strip.frame_start) < self.frame_start:
frame_start = self.frame_start
frame_end += int(strip.frame_final_start)
if frame_end > self.frame_end:
frame_end = self.frame_end
create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}',
start=frame_start,
end=frame_end,
channel=self.destination_channel_name
)
i += 1
frame_start = frame_end
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
#process.wait()
# Last strip:
if frame_start < self.frame_end:
create_shot_strip(
f'tmp_shot_{str(i).zfill(3)}',
start=frame_start,
end=self.frame_end,
channel=self.destination_channel_name
)
return {'FINISHED'}
class VSETB_OT_set_stamps(Operator):
bl_idname = "vse_toolbox.set_stamps"
bl_label = "Set Stamps"
@@ -381,22 +182,12 @@ 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.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'
scn.sequence_editor.sequences.remove(strip)
bpy.ops.sequencer.select_all(action='DESELECT')
@@ -409,9 +200,7 @@ class VSETB_OT_set_stamps(Operator):
crop_x = int(width * 0.4)
crop_max_y = int(height - font_size*2)
# Use temporary high channels for text strips before meta grouping
tmp_base = stamps_channel + 1
#crop_min_y = int(scn.render.resolution_y * 0.01)
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))
@@ -419,38 +208,37 @@ 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=tmp_base, **stamp_params,
text=project_text, x=0.01, anchor_x='LEFT', anchor_y='BOTTOM')
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_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=tmp_base + 1, **stamp_params,
text='{sequence_strip} / {strip}', anchor_y='BOTTOM')
shot_strip_stamp = new_text_strip('shot_stamp', channel=2, **stamp_params,
text='{sequence} / sh{shot}', align_y='BOTTOM')
shot_strip_stamp.crop.min_x = crop_x
shot_strip_stamp.crop.max_x = crop_x
shot_strip_stamp.crop.max_y = crop_max_y
# Frame
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 = 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.crop.min_x = crop_x *2
frame_strip_stamp.crop.max_y = crop_max_y
bpy.ops.sequencer.meta_make()
stamps_strip = context.active_strip
stamps_strip = context.active_sequence_strip
stamps_strip.name = 'Stamps'
stamps_strip.channel = stamps_channel
stamps_strip.channel = channel_index
#stamps_strip = scn.sequence_editor.sequences.new_meta('Stamps', scn.frame_start, scn.frame_end)
#stamps_strip.channel = get_channel_index('Stamps')
# 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)
scn.frame_set(scn.frame_current) # For update stamps
return {"FINISHED"}
@@ -508,54 +296,39 @@ class VSETB_OT_next_shot(Operator):
return {"FINISHED"}
class VSETB_OT_open_strip_folder(Operator):
bl_idname = "vse_toolbox.open_strip_folder"
bl_label = "Open Strip Folder"
bl_description = "Open selected strip folder"
class VSETB_OT_open_shot_folder(Operator):
bl_idname = "vse_toolbox.open_shot_folder"
bl_label = "Open Shot Folder"
bl_description = "Open Shot Folder"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
strip = context.active_strip
if not strip:
cls.poll_message_set('No active')
settings = get_scene_settings()
project = settings.active_project
if not project.templates.get('shot_dir'):
cls.poll_message_set('No shot_dir template setted')
return
if not any(p in get_channel_name(strip) for p in ('Movie', 'Video', 'Audio', 'Sound')):
cls.poll_message_set('No active Movie or Audio strip')
strip = context.active_sequence_strip
if not strip or get_channel_name(strip) != 'Shots':
cls.poll_message_set('No shot strip active')
return
return True
def execute(self, context):
tpl_by_channel = {
'Shots': 'shot_dir',
'Sequences': 'sequence_dir'
}
strip = context.active_strip
strip = context.active_sequence_strip
settings = get_scene_settings()
project = settings.active_project
strip_settings = get_strip_settings()
format_data = {**settings.format_data, **project.format_data, **strip_settings.format_data}
channel_name = get_channel_name(strip)
if strip.type in ('MOVIE', 'IMAGE'):
path = Path(strip.filepath)
elif strip.type in ('SOUND'):
path = Path(strip.sound.filepath)
else:
folder_template = expandvars(project.templates[tpl_by_channel[channel_name]].value)
path = Path(folder_template.format(**format_data))
if not path.is_dir():
path = path.parent
bpy.ops.wm.path_open(filepath=str(path))
shot_folder_template = expandvars(project.templates['shot_dir'].value)
shot_folder_path = shot_folder_template.format(**format_data)
bpy.ops.wm.path_open(filepath=shot_folder_path)
return {"FINISHED"}
@@ -577,12 +350,12 @@ class VSETB_OT_collect_files(Operator):
cls.poll_message_set('Save the blend to collect files')
def execute(self, context):
strip = context.active_strip
strip = context.active_sequence_strip
settings = get_scene_settings()
project = settings.active_project
strips = [s for s in context.scene.sequence_editor.strips_all if s.type in ('MOVIE', 'SOUND')]
strips = [s for s in context.scene.sequence_editor.sequences_all if s.type in ('MOVIE', 'SOUND')]
context.window_manager.progress_begin(0, len(strips))
for i, strip in enumerate(strips):
@@ -624,91 +397,6 @@ class VSETB_OT_collect_files(Operator):
return context.window_manager.invoke_props_dialog(self)
class VSETB_OT_insert_channel(Operator):
bl_idname = "vse_toolbox.insert_channel"
bl_label = "Insert Channel"
bl_description = "Insert a Channel bellow the active strip"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.active_strip
def execute(self, context):
scn = context.scene
channel_index = context.active_strip.channel
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]
#i = list(scn.sequence_editor.channels).index(i)
prev_channel = channels.get(i-1)
if i == channel_index:
channel.name = "Channel"
channel.lock = False
channel.mute = False
elif i >= channel_index and prev_channel is not None:
prev_name, prev_lock, prev_mute = prev_channel
channel.name = prev_name
if channel.lock != prev_lock:
channel.lock = prev_lock
if channel.mute != prev_mute:
channel.mute = prev_mute
return {'FINISHED'}
class VSETB_OT_remove_channel(Operator):
bl_idname = "vse_toolbox.remove_channel"
bl_label = "Remove Channel"
bl_description = "Remove Channel bellow the active strip"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.active_strip
def execute(self, context):
scn = context.scene
channel_index = context.active_strip.channel
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.strips)
for strip in sorted(strips, key=lambda x: x.channel):
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(), reverse=True):
channel = scn.sequence_editor.channels[i]
#i = list(scn.sequence_editor.channels).index(i)
prev_channel = channels.get(i+1)
if i >= channel_index-1 and prev_channel is not None:
prev_name, prev_lock, prev_mute = prev_channel
channel.name = prev_name
if channel.lock != prev_lock:
channel.lock = prev_lock
if channel.mute != prev_mute:
channel.mute = prev_mute
return {'FINISHED'}
class WM_OT_split_view(Operator):
"""Toggle Split sequencer view"""
@@ -775,177 +463,19 @@ class WM_OT_split_view(Operator):
return {"FINISHED"}
'''
class VSETB_OT_set_review_workspace(Operator):
"""Set Review Workspace"""
class VSETB_OT_merge_shot_strips(Operator):
"""Merge selected shots strips (required at least two strips)."""
bl_idname = "vse_toolbox.set_workspace"
bl_label = "Set Review Workspace"
bl_idname = "vse_toolbox.merge_shot_strips"
bl_label = "Merge Shot Strips"
@classmethod
def poll(cls, context):
selected_strips = bpy.context.selected_strips
if len(selected_strips) <= 1:
return False
return all(get_channel_name(strip) == 'Shots' for strip in selected_strips)
def execute(self, context):
bpy.ops.workspace.append_activate(idname='Review', filepath=str(REVIEW_TEMPLATE_BLEND))
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.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="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 = []
@@ -965,9 +495,6 @@ def register_keymaps():
kmi = km.keymap_items.new('vse_toolbox.next_shot', type='RIGHT_ARROW', value='PRESS', ctrl=True)
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new('vse_toolbox.merge_shot_strips', type='J', value='PRESS', ctrl=True)
addon_keymaps.append((km, kmi))
def unregister_keymaps():
#print('unregister_keymaps', addon_keymaps)
@@ -982,19 +509,13 @@ def unregister_keymaps():
classes = (
VSETB_OT_rename,
VSETB_OT_set_sequencer,
VSETB_OT_scene_cut_detection,
VSETB_OT_set_stamps,
VSETB_OT_show_waveform,
VSETB_OT_previous_shot,
VSETB_OT_next_shot,
VSETB_OT_open_strip_folder,
VSETB_OT_open_shot_folder,
VSETB_OT_collect_files,
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,
WM_OT_split_view
)
def register():
+5 -9
View File
@@ -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.strips
sequencer = scn.sequence_editor.sequences
assets_missing = set()
@@ -355,9 +355,7 @@ class VSETB_OT_import_spreadsheet(Operator):
#print(SPREADSHEET[:2])
cell_types = project.get_cell_types()
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}
cell_names = {k: spreadsheet.cells[k].import_name for k in header if k}
#separator = spreadsheet.separator.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
@@ -366,12 +364,10 @@ class VSETB_OT_import_spreadsheet(Operator):
frame_start = scn.frame_start
for row in SPREADSHEET[1:]:
#print(row)
cell_data = {cell_names[k]: v for k, v in zip(header, row) if k in cell_names}
cell_data = {cell_names[k]: v for k, v in zip(header, row) if k}
#cell_data = {k: v for k, v in zip(header, row)}
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 shot_strips if s.name == shot_name), None)
@@ -397,7 +393,7 @@ class VSETB_OT_import_spreadsheet(Operator):
type='COLOR',
channel=channel,
frame_start=frame_start,
length=frame_end - frame_start,
frame_end=frame_end,
)
strip.blend_alpha = 0.0
+14 -93
View File
@@ -160,15 +160,6 @@ class VSETB_OT_load_projects(Operator):
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)
@@ -205,7 +196,6 @@ class VSETB_OT_load_projects(Operator):
#print(metadata_data)
task_status = project.task_statuses.add()
task_status.name = status_data['short_name'].upper()
task_status.is_done = status_data.get('is_done', False)
project.task_types.clear()
for task_type_data in tracker.get_shot_task_types(project_data):
@@ -229,10 +219,7 @@ class VSETB_OT_load_projects(Operator):
if project.name not in project_names:
settings.projects.remove(list(settings.projects).index(project))
# ``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'):
if self.ctrl or not settings.get('projects_loaded'):
bpy.ops.vse_toolbox.load_settings()
if prev_project_name != '/' and prev_project_name in settings.projects:
@@ -270,24 +257,15 @@ class VSETB_OT_new_episode(Operator):
settings = get_scene_settings()
prefs = get_addon_prefs()
tracker = prefs.tracker
project = settings.active_project
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:
episode_name = settings.episode_template.format(index=int(self.episode_name))
episode = tracker.get_episode(episode_name)
if episode:
self.report({'ERROR'}, f'Episode {episode_name} already exists')
return {"CANCELLED"}
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']
tracker.new_episode(episode_name)
tracker.update_project()
self.report({'INFO'}, f'Episode {episode_name} successfully created')
@@ -297,19 +275,11 @@ class VSETB_OT_new_episode(Operator):
class VSETB_OT_upload_to_tracker(Operator):
bl_idname = "vse_toolbox.upload_to_tracker"
bl_label = "Upload 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_description = "Upload selected strip to tracker"
bl_options = {"REGISTER", "UNDO"}
@classmethod
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
def invoke(self, context, event):
@@ -320,13 +290,6 @@ class VSETB_OT_upload_to_tracker(Operator):
tracker = prefs.tracker
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()}"
return context.window_manager.invoke_props_dialog(self, width=350)
@@ -382,35 +345,16 @@ class VSETB_OT_upload_to_tracker(Operator):
format_data = {**settings.format_data, **self.project.format_data}
status = upload_to_tracker.status
keep_current_status = status == 'CURRENT'
if keep_current_status:
if status == 'CURRENT':
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)
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))
for i, strip in enumerate(shot_strips):
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)
@@ -427,11 +371,7 @@ class VSETB_OT_upload_to_tracker(Operator):
task = tracker.get_task(upload_to_tracker.task, entity=shot)
if not task:
task = tracker.new_task(
shot,
task_type=upload_to_tracker.task,
status=default_new_task_status or status,
)
task = tracker.new_task(shot, task_type=upload_to_tracker.task)
preview = None
if upload_to_tracker.add_preview:
@@ -441,7 +381,7 @@ class VSETB_OT_upload_to_tracker(Operator):
preview_template = expand(upload_to_tracker.render_strip_template, **project_templates)
strip_data['ext'] = 'mov'
else:
preview_template = expand(self.project.render_video_strip_template, **project_templates)
preview_template = expandvars(self.project.render_video_strip_template)
preview = preview_template.format(**strip_data)
preview = Path(os.path.abspath(bpy.path.abspath(preview)))
@@ -461,7 +401,6 @@ 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:
@@ -471,7 +410,7 @@ class VSETB_OT_upload_to_tracker(Operator):
comment=comment_data,
preview=preview)
if upload_to_tracker.set_main_preview and preview_data:
if upload_to_tracker.set_main_preview:
bpy.app.timers.register(partial(tracker.set_main_preview, preview_data), first_interval=10)
params = {}
@@ -480,14 +419,7 @@ class VSETB_OT_upload_to_tracker(Operator):
params['description'] = strip_settings.description
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.setdefault('custom_data', {}).update({
'frame_in': strip.frame_final_start,
'frame_out': strip.frame_final_end - 1,
})
if params:
tracker.update_data(shot, **params)
@@ -499,18 +431,11 @@ class VSETB_OT_upload_to_tracker(Operator):
if upload_to_tracker.tasks_comment:
for task_type in self.project.task_types:
task_settings = getattr(strip_settings.tasks, norm_name(task_type.name))
if not task_settings.comment:
continue
task = getattr(strip_settings.tasks, norm_name(task_type.name))
tracker_task = tracker.get_task(task_type.name, entity=shot)
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)
if task.comment and tracker_task.get('last_comment') != task.comment:
tracker.new_comment(tracker_task, comment=task.comment)
context.window_manager.progress_end()
@@ -524,10 +449,6 @@ class VSETB_OT_open_shot_on_tracker(Operator):
@classmethod
def poll(cls, context):
if not get_strips(channel='Shots', selected_only=True):
cls.poll_message_set('No active Shots strip')
return
prefs = get_addon_prefs()
if prefs.tracker:
return True
Binary file not shown.
+16 -47
View File
@@ -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', package_name='gazu>=1.0,<2.0')
gazu = install_module('gazu')
except Exception as e:
print('Could not install gazu')
print(e)
@@ -32,15 +32,10 @@ class Kitsu(Tracker):
admin_password : StringProperty(subtype='PASSWORD')
def admin_connect(self):
url = expandvars(self.url)
if not url:
raise ConnectionError("No Kitsu URL configured")
url = self.url
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)
@@ -120,11 +115,6 @@ class Kitsu(Tracker):
return os.environ['TRACKER_PROJECT_NAME']
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()
def get_episode(self, episode, project=None):
@@ -282,7 +272,7 @@ class Kitsu(Tracker):
entity = self.get_id(entity)
task_type = self.get_task_type(task)
task = gazu.task.get_task_by_entity(entity, task_type)
task = gazu.task.get_task_by_name(entity, task_type)
if not task:
return
@@ -357,26 +347,6 @@ 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)
@@ -420,28 +390,27 @@ class Kitsu(Tracker):
def update_data(self, entity, custom_data={}, name=None, description=None, frames=None, clear=False):
if isinstance(entity, dict):
entity_id = entity['id']
existing_data = entity.get('data') or {}
else:
entity_id = self.get_id(entity)
existing_data = None
entity = gazu.client.fetch_one('entities', entity_id)
payload = {}
if name:
payload['name'] = name
entity['name'] = name
if description:
payload['description'] = description
entity['description'] = description
if frames:
payload['nb_frames'] = frames
entity['nb_frames'] = frames
if custom_data or clear:
if clear:
payload['data'] = dict(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}
if clear or not entity['data']:
entity['data'] = custom_data
else:
entity['data'].update(custom_data)
entity_data = gazu.client.put(f"data/entities/{entity_id}", payload)
#print('######UPDATE DATA')
#pprint(entity)
entity_data = gazu.client.put(f"data/entities/{entity_id}", entity)
#print()
#pprint(entity)
return entity_data['data']
-148
View File
@@ -1,148 +0,0 @@
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))
+33 -94
View File
@@ -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, anchor_x='CENTER', anchor_y='CENTER', select=False,
x=0.5, y=0.5, align_x='CENTER', align_y='CENTER', select=False,
box_color=None, box_margin=0.005):
strips = bpy.context.scene.sequence_editor.strips
strip = strips.new_effect(name, 'TEXT', channel, frame_start=start, length=end - start)
sequences = bpy.context.scene.sequence_editor.sequences
strip = sequences.new_effect(name, 'TEXT', channel, frame_start=start, frame_end=end)
strip.select = select
strip.text = text
strip.location.x = x
strip.location.y = y
strip.anchor_y = anchor_y
strip.anchor_x = anchor_x
strip.align_y = align_y
strip.align_x = align_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.strips if s.channel==channel]
strips = [s for s in scn.sequence_editor.sequences if s.channel==channel]
if selected_only:
strips = [s for s in strips if s.select]
@@ -103,27 +103,11 @@ def get_channel_name(strip):
return scn.sequence_editor.channels[strip.channel].name
def get_strip_sequence_name(strip):
"""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,
)
sequence_strip = get_strip_at(channel='Sequences', frame=strip.frame_final_start)
if sequence_strip:
return sequence_strip.name
return 'NoSequence'
else:
return 'NoSequence'
def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_sequence=False):
scn = bpy.context.scene
@@ -137,24 +121,12 @@ def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_
prev_sequence_name = None
strip_number = 0
planned_names = []
for strip in strips:
channel = get_channel_name(strip)
sequence_name = get_strip_sequence_name(strip)
format_data = {}
if channel == 'Shots':
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)
format_data = parse(project.sequence_template, sequence_name)
else:
format_data['sequence'] = str(strip_number*increment + start_number).zfill(padding)
@@ -169,19 +141,16 @@ def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_
name = template.format(**format_data)
planned_names.append((strip, name))
prev_sequence_name = sequence_name
strip_number += 1
existing_strip = scn.sequence_editor.sequences_all.get(name)
if existing_strip:
existing_strip.name = f"{name}_tmp"
# 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}')
strip.name = name
prev_sequence_name = sequence_name
strip_number += 1
def set_channels():
scn = bpy.context.scene
settings = get_scene_settings()
@@ -330,10 +299,9 @@ def render_scene(output, attributes=None):
(scn.render, "filepath", output),
]
print(f'Render Strip to {scn.render.filepath}')
Path(output).parent.mkdir(exist_ok=True, parents=True)
with attr_set(attributes):
print(f'Render Strip to {scn.render.filepath}')
bpy.ops.render.opengl(animation=True, sequencer=True)
bpy.ops.render.opengl(animation=True, sequencer=True)
def render_strip(strip, output, attributes=None):
output = os.path.abspath(bpy.path.abspath(str(output)))
@@ -357,12 +325,11 @@ def render_strip(strip, output, attributes=None):
def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
opentimelineio = install_module('opentimelineio')
install_module('opentimelineio', package_name='opentimelineio-plugins')
from opentimelineio.schema import Clip
scn = bpy.context.scene
sequencer = scn.sequence_editor.strips
sequencer = scn.sequence_editor.sequences
strips = get_strips(channel='Shots')
shot_channel = get_channel_index('Shots')
@@ -372,31 +339,23 @@ 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, **extra_kwargs)
str(edl), adapter, rate=scn.render.fps, ignore_timecode_mismatch=True)
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, **extra_kwargs)
data, adapter, rate=scn.render.fps, ignore_timecode_mismatch=True)
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))
scn.frame_start = (
0 if timeline.global_start_time is None else timeline.global_start_time
)
# Get all video clips only
clips = []
for track in timeline.tracks:
for child in track.find_clips(shallow_search=True):
for child in track.each_child(shallow_search=True):
# FIXME Exclude Gaps for now. Gaps are Transitions, Blank Spaces...
if not isinstance(child, Clip):
@@ -406,16 +365,12 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
if any(child.name.lower().endswith(ext) for ext in SOUND_SUFFIXES):
continue
if [c for c in clips if child.range_in_parent() == c.range_in_parent()]:
continue
clips.append(child)
clips.sort(key=lambda x: x.range_in_parent().start_time)
for i, clip in enumerate(clips):
#print(clip)
frame_start = opentimelineio.opentime.to_frames(
clip.range_in_parent().start_time)
@@ -434,7 +389,6 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
print(f'No strip found for {clip.name}')
if strip:
#print('Strip already existing', clip)
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')
#self.report({'INFO'}, f'The strip {strip.name} is updated with new range')
@@ -444,13 +398,13 @@ def import_edit(filepath, adapter="cmx_3600", channel='Shots', match_by='name'):
strip.frame_final_end = frame_end
else:
print('Create a new strip', clip)
print('Create a new strip')
strip = sequencer.new_effect(
name=clip.name,
type='COLOR',
channel=shot_channel,
frame_start=frame_start,
length=frame_end - frame_start,
frame_end=frame_end,
)
strip.blend_alpha = 0.0
@@ -478,7 +432,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.strips.new_movie(
strip = scn.sequence_editor.sequences.new_movie(
name=Path(filepath).stem,
filepath=str(filepath),
channel=get_channel_index('Movie'),
@@ -519,7 +473,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.strips.new_sound(
strip = scn.sequence_editor.sequences.new_sound(
name=f'{filepath.stem} Audio',
filepath=str(filepath),
channel=get_channel_index('Audio'),
@@ -534,7 +488,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.strips) + 1
return max(s.channel for s in bpy.context.scene.sequence_editor.sequences) + 1
def clean_sequencer(edit=False, movie=False, sound=False):
scn = bpy.context.scene
@@ -548,7 +502,7 @@ def clean_sequencer(edit=False, movie=False, sound=False):
sequences.extend(get_strips('Audio'))
for sequence in sequences:
scn.sequence_editor.strips.remove(sequence)
scn.sequence_editor.sequences.remove(sequence)
@persistent
def set_active_strip(scene):
@@ -570,7 +524,7 @@ def set_active_strip(scene):
@persistent
def update_text_strips(scene):
if not scene.sequence_editor or not scene.sequence_editor.strips_all:
if not scene.sequence_editor or not scene.sequence_editor.sequences_all:
return
#print("update_text_strips")
@@ -616,7 +570,7 @@ def update_text_strips(scene):
})
format_data.update(shot_strip.vsetb_strip_settings.format_data)
for strip in scene.sequence_editor.strips_all:
for strip in scene.sequence_editor.sequences_all:
if not strip.type == 'TEXT':
continue
@@ -628,18 +582,3 @@ def update_text_strips(scene):
if 'text_pattern' in strip.keys():
strip.text = strip['text_pattern'].format_map(MissingKey(**format_data))
def create_shot_strip(name, start, end, channel='Shots'):
shot_strip = bpy.context.scene.sequence_editor.strips.new_effect(
name,
'COLOR',
get_channel_index(channel),
frame_start=start,
length=end - start
)
shot_strip.blend_alpha = 0
shot_strip.color = (0.5, 0.5, 0.5)
return shot_strip
+95 -204
View File
@@ -6,9 +6,9 @@ import bpy
from bpy.types import Panel, Menu
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.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
@@ -47,23 +47,21 @@ class VSETB_PT_main(VSETB_main, Panel):
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.prop(settings, "project_name", text="")
row.prop(settings, 'project_name', text='')
project = settings.active_project
if not project:
return
if project and project.type == "TVSHOW":
if project and project.type == 'TVSHOW':
row.separator(factor=0.5)
row.prop(project, "episode_name", text="")
row.prop(project, 'episode_name', text='')
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:
box = layout.box()
split = box.split(factor=0.3)
@@ -78,7 +76,7 @@ class VSETB_PT_main(VSETB_main, Panel):
row.separator(factor=0.5)
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):
row = name_col.row()
@@ -88,9 +86,8 @@ class VSETB_PT_main(VSETB_main, Panel):
subrow = row.row()
subrow.prop(template, "value", text="")
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()
# prefs = get_addon_prefs()
@@ -106,13 +103,13 @@ class VSETB_PT_main(VSETB_main, Panel):
# if project.type == 'TVSHOW':
# 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:
box = col.box()
col = box.column(align=True)
@@ -129,22 +126,21 @@ class VSETB_PT_main(VSETB_main, Panel):
#col.prop(project, 'shot_template')
# col.separator()
# col.operator('vse_toolbox.new_episode', text='Add Episode', icon='IMPORT')
"""
'''
# Rename
class VSETB_PT_strip(Panel):
bl_space_type = "SEQUENCE_EDITOR"
bl_region_type = "UI"
bl_category = "Strip"
bl_label = "VSE ToolBox"
# bl_order = 0
#bl_order = 0
@classmethod
def poll(cls, context):
strip = context.active_strip
return strip and get_channel_name(strip) == "Shots"
strip = context.active_sequence_strip
return strip and get_channel_name(strip) == 'Shots'
def draw(self, context):
prefs = get_addon_prefs()
@@ -154,28 +150,22 @@ class VSETB_PT_strip(Panel):
layout.use_property_split = True
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):
bl_label = "Sequencer"
# bl_parent_id = "VSETB_PT_main"
#bl_parent_id = "VSETB_PT_main"
def draw_header_preset(self, context):
settings = get_scene_settings()
audio_strips = [s for s in get_strips("Audio") if s.type == "SOUND"]
audio_strips = get_strips('Audio')
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)
def draw(self, context):
@@ -184,36 +174,22 @@ class VSETB_PT_sequencer(VSETB_main, Panel):
settings = get_scene_settings()
project = settings.active_project
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 = get_channel_name(strip) or "Selected Channel"
strip = context.active_sequence_strip
channel = get_channel_name(strip)
col = layout.column()
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.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"
)
col.operator('vse_toolbox.set_sequencer', text='Set-Up Sequencer', icon='SEQ_SEQUENCER')
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.collect_files", text='Collect Files', icon='PACKAGE')
class VSETB_PT_settings(VSETB_main, Panel):
bl_label = "Settings"
# bl_parent_id = "VSETB_PT_main"
bl_options = {"DEFAULT_CLOSED"}
#bl_parent_id = "VSETB_PT_main"
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)
def draw(self, context):
@@ -223,21 +199,19 @@ class VSETB_PT_settings(VSETB_main, Panel):
project = settings.active_project
col = layout.column()
# row = col.row(align=True)
col.prop(project, "sequence_template")
col.prop(project, "shot_template")
col.prop(project, "render_template")
#row = col.row(align=True)
col.prop(project, 'sequence_template')
col.prop(project, 'shot_template')
col.prop(project, 'render_template')
class VSETB_PT_imports(VSETB_main, Panel):
bl_label = "Imports"
# bl_parent_id = "VSETB_PT_main"
# bl_options = {'DEFAULT_CLOSED'}
#bl_parent_id = "VSETB_PT_main"
#bl_options = {'DEFAULT_CLOSED'}
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):
prefs = get_addon_prefs()
@@ -246,35 +220,26 @@ class VSETB_PT_imports(VSETB_main, Panel):
project = settings.active_project
col = layout.column()
# row = col.row(align=True)
# 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"
)
#row = col.row(align=True)
#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")
class VSETB_PT_presets(PresetPanel, Panel):
bl_label = "Spreadsheet Presets"
preset_subdir = "vse_toolbox"
preset_operator = "script.execute_preset"
bl_label = 'Spreadsheet Presets'
preset_subdir = 'vse_toolbox'
preset_operator = 'script.execute_preset'
preset_add_operator = "vse_toolbox.add_spreadsheet_preset"
class VSETB_PT_exports(VSETB_main, Panel):
bl_label = "Exports"
# bl_parent_id = "VSETB_PT_main"
bl_options = {"DEFAULT_CLOSED"}
#bl_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'}
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):
prefs = get_addon_prefs()
@@ -284,48 +249,39 @@ class VSETB_PT_exports(VSETB_main, Panel):
col = layout.column(align=False)
# 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("_", " ")
col.operator(
"vse_toolbox.upload_to_tracker",
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")
tracker_label = settings.tracker_name.title().replace('_', ' ')
col.operator('vse_toolbox.upload_to_tracker', 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):
bl_label = "Tracker"
# bl_parent_id = "VSETB_PT_main"
bl_options = {"DEFAULT_CLOSED"}
#bl_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.active_strip
return context.active_sequence_strip
def draw_header_preset(self, context):
active_strip = context.active_strip
active_strip = context.active_sequence_strip
self.layout.label(text=active_strip.name)
def draw(self, context):
return
class VSETB_PT_casting(VSETB_main, Panel):
bl_label = "Casting"
bl_parent_id = "VSETB_PT_tracker"
bl_options = {"DEFAULT_CLOSED"}
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
strip = context.active_strip
return strip and get_channel_name(strip) == "Shots"
strip = context.active_sequence_strip
return strip and get_channel_name(strip) == 'Shots'
def draw(self, context):
layout = self.layout
@@ -340,39 +296,25 @@ class VSETB_PT_casting(VSETB_main, Panel):
if not project.assets:
row = layout.row(align=True)
row.label(text="No Assets in this Project")
row.label(text='No Assets in this Project')
else:
row = layout.row()
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.operator("vse_toolbox.casting_add", icon="ADD", text="")
col_tool.operator("vse_toolbox.casting_remove", icon="REMOVE", text="")
col_tool.operator('vse_toolbox.casting_add', icon='ADD', text="")
col_tool.operator('vse_toolbox.casting_remove', icon='REMOVE', text="")
col_tool.separator()
col_tool.operator(
"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.operator('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.operator("vse_toolbox.copy_casting", icon="COPYDOWN", text="")
col_tool.operator("vse_toolbox.paste_casting", icon="PASTEDOWN", text="")
col_tool.operator('vse_toolbox.copy_casting', icon='COPYDOWN', text="")
col_tool.operator('vse_toolbox.paste_casting', icon='PASTEDOWN', text="")
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="")
col_tool.operator('vse_toolbox.casting_replace', icon='ZOOM_ALL', text="")
if strip_settings.casting:
casting_item = strip_settings.casting[strip_settings.casting_index]
@@ -380,7 +322,7 @@ class VSETB_PT_casting(VSETB_main, Panel):
if asset:
if asset.icon_id:
row = col.row(align=True)
# row.scale_y = 0.5
#row.scale_y = 0.5
# box = col.box()
# box.template_icon(icon_value=ico.icon_id, scale=7.5)
@@ -390,11 +332,11 @@ class VSETB_PT_casting(VSETB_main, Panel):
class VSETB_PT_metadata(VSETB_main, Panel):
bl_label = "Shot Metadata"
bl_parent_id = "VSETB_PT_tracker"
bl_options = {"DEFAULT_CLOSED"}
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.active_strip and get_scene_settings().active_project
return context.active_sequence_strip and get_scene_settings().active_project
def draw(self, context):
@@ -404,11 +346,11 @@ class VSETB_PT_metadata(VSETB_main, Panel):
layout = self.layout
row = layout.row(align=True)
label_col = row.column(align=True)
label_col.alignment = "RIGHT"
label_col.alignment = 'RIGHT'
field_col = row.column(align=True)
for metadata_type in project.metadata_types:
if metadata_type.entity_type != "SHOT":
if metadata_type.entity_type != 'SHOT':
continue
metadata_key = metadata_type.field_name
@@ -420,47 +362,41 @@ class VSETB_PT_metadata(VSETB_main, Panel):
if metadata_type.choices:
metadata_value = getattr(strip_settings.metadata, metadata_key)
icon = "LAYER_USED"
icon = 'LAYER_USED'
if metadata_value:
if metadata_value in metadata_type.choices:
icon = "DOT"
icon = 'DOT'
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:
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):
bl_label = "Comments"
bl_parent_id = "VSETB_PT_tracker"
bl_options = {"DEFAULT_CLOSED"}
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.active_strip and get_scene_settings().active_project
return context.active_sequence_strip and get_scene_settings().active_project
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
label_col = row.column(align=True)
label_col.alignment = "RIGHT"
label_col.alignment = 'RIGHT'
field_col = row.column(align=True)
strip_settings = get_strip_settings()
project = get_scene_settings().active_project
for task_type in project.task_types:
norm_task_name = norm_str(task_type.name)
@@ -473,20 +409,17 @@ class VSETB_PT_comments(VSETB_main, Panel):
row = field_col.row(align=True)
row.separator()
sub = row.row(align=True)
sub.alignment = "LEFT"
sub.alignment = 'LEFT'
sub.scale_x = 0.15
sub.prop(task_type, "color", text="")
sub.prop(task_type, 'color', text='')
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):
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
settings = get_strip_settings()
@@ -499,9 +432,7 @@ def context_menu_prop(self, context):
layout = self.layout
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):
@@ -510,52 +441,18 @@ class VSETB_MT_main_menu(Menu):
def draw(self, context):
layout = self.layout
op = layout.operator(
"workspace.append_activate", text="Set Review Workspace", icon="WORKSPACE"
)
op.idname = "Review"
op = layout.operator('workspace.append_activate', text='Set Review Workspace', icon="WORKSPACE")
op.idname = 'Review'
op.filepath = str(REVIEW_TEMPLATE_BLEND)
layout.operator("wm.split_view", icon="ARROW_LEFTRIGHT")
layout.separator()
layout.operator(
"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.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",
)
layout.operator('vse_toolbox.open_shot_folder', text='Open Shot Folder', icon='FILE_FOLDER')
layout.operator('vse_toolbox.open_shot_on_tracker', text='Open Shot on Tracker', icon='URL')
def draw_vse_toolbox_menu(self, context):
self.layout.menu("VSETB_MT_main_menu")
def draw_file_new(self, context):
self.layout.separator()
op = self.layout.operator("wm.read_homefile", text="Review")
op.filepath = str(REVIEW_TEMPLATE_BLEND)
op.load_ui = True
classes = (
VSETB_PT_main,
@@ -568,10 +465,9 @@ classes = (
VSETB_PT_presets,
VSETB_PT_exports,
VSETB_PT_strip,
VSETB_MT_main_menu,
VSETB_MT_main_menu
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
@@ -579,14 +475,9 @@ def register():
bpy.types.UI_MT_button_context_menu.append(context_menu_prop)
bpy.types.SEQUENCER_MT_editor_menus.append(draw_vse_toolbox_menu)
bpy.types.TOPBAR_MT_file_new.append(draw_file_new)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
bpy.types.UI_MT_button_context_menu.remove(context_menu_prop)
bpy.types.SEQUENCER_MT_editor_menus.remove(draw_vse_toolbox_menu)
bpy.types.TOPBAR_MT_file_new.remove(draw_file_new)
+2 -23
View File
@@ -64,18 +64,6 @@ def load_trackers():
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():
prefs = get_addon_prefs()
prefs_config_file = prefs.config_path
@@ -103,16 +91,7 @@ def load_prefs():
continue
setattr(tracker_pref, k, os.path.expandvars(v))
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)
prefs['tracker_name'] = prefs_datas['tracker_name']
class Trackers(PropertyGroup):
@@ -176,7 +155,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()
+8 -32
View File
@@ -119,7 +119,6 @@ class Sequence(PropertyGroup):
class TaskStatus(PropertyGroup):
is_done : BoolProperty(default=False)
__annotations__ = {}
@@ -253,7 +252,6 @@ class ImportShots(PropertyGroup):
#shot_folder_template: StringProperty(
# name="Shot Template", default="$PROJECT_ROOT/sequences/sq{sequence}/sh{shot}")
clear: BoolProperty(default=True)
video_template : StringProperty(
name="Video Path", default="//sources/{sequence}_{shot}_{task}.{ext}")
@@ -268,8 +266,7 @@ class ImportShots(PropertyGroup):
class UploadToTracker(PropertyGroup):
render_strips: BoolProperty(default=False,
description='Render each selected shot strip to the Movie Path before uploading it as the preview')
render_strips: BoolProperty(default=False)
render_strip_template : StringProperty(
name="Movie Path", default="//render/{strip}.{ext}")
@@ -301,28 +298,7 @@ def get_episodes_items(self, context):
def on_episode_updated(self, context):
settings = get_scene_settings()
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}')
os.environ['TRACKER_EPISODE_ID'] = settings.active_episode.id
class Project(PropertyGroup):
id : StringProperty(default='')
@@ -738,7 +714,7 @@ class VSETB_PGT_strip_settings(PropertyGroup):
@property
def strip(self):
sequences = bpy.context.scene.sequence_editor.strips_all
sequences = bpy.context.scene.sequence_editor.sequences_all
return next(s for s in sequences if s.vsetb_strip_settings == self)
@property
@@ -751,7 +727,7 @@ class VSETB_PGT_strip_settings(PropertyGroup):
channel = get_channel_name(strip)
if channel == 'Sequences':
data = parse(project.sequence_template, strip.name) or {}
data = parse(project.sequence_template, strip.name)
#data['index'] = int(data['index'])
#data['sequence'] = strip.name
data['strip'] = strip.name
@@ -760,10 +736,10 @@ class VSETB_PGT_strip_settings(PropertyGroup):
elif channel == "Shots":
data = {}
if sequence_strip_name := get_strip_sequence_name(strip):
data = parse(project.sequence_template, sequence_strip_name) or {}
data = parse(project.sequence_template, sequence_strip_name)
data['sequence_strip'] = sequence_strip_name
data.update(parse(project.shot_template, strip.name) or {})
data.update(parse(project.shot_template, strip.name))
#data['index'] = int(data['index'])
data['strip'] = strip.name
@@ -831,7 +807,7 @@ def register():
bpy.utils.register_class(cls)
bpy.types.Scene.vsetb_settings = PointerProperty(type=VSETB_PGT_scene_settings)
bpy.types.Strip.vsetb_strip_settings = PointerProperty(type=VSETB_PGT_strip_settings)
bpy.types.Sequence.vsetb_strip_settings = PointerProperty(type=VSETB_PGT_strip_settings)
#load_metadata_types()
bpy.app.handlers.load_post.append(load_handler)
@@ -841,7 +817,7 @@ def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Strip.vsetb_strip_settings
del bpy.types.Sequence.vsetb_strip_settings
del bpy.types.Scene.vsetb_settings
bpy.app.handlers.load_post.remove(load_handler)