rewrite import_shots and cleanup

pull/5/head
ChristopheSeux 2024-04-10 16:15:57 +02:00
parent dae9d55494
commit 2960bab63d
15 changed files with 1068 additions and 269 deletions

View File

@ -5,13 +5,11 @@ bl_info = {
"description": "In-House Video editing Tools",
"author": "Samuel Bernou, Clement Ducarteron, Christophe Seux",
"version": (0, 1, 0),
"blender": (3, 4, 1),
"blender": (4, 0, 2),
"location": "Sequencer",
"warning": "",
"category": "Sequencer",
}
# "doc_url": "https://github.com/Pullusb/REPONAME",
# "tracker_url": "https://github.com/Pullusb/REPONAME/issues",
import sys
from pathlib import Path
@ -21,6 +19,7 @@ from vse_toolbox import ui
from vse_toolbox import operators
from vse_toolbox.constants import ASSET_PREVIEWS
from vse_toolbox.sequencer_utils import set_active_strip, update_text_strips
from vse_toolbox.bl_utils import get_addon_prefs
modules = (
@ -35,6 +34,7 @@ if 'bpy' in locals():
import bpy
def register():
bpy.app.handlers.frame_change_post.append(update_text_strips)
bpy.app.handlers.render_pre.append(update_text_strips)
@ -44,6 +44,8 @@ def register():
bpy.app.handlers.frame_change_post.append(set_active_strip)
prefs = get_addon_prefs()
#print('\n\n-------------------', prefs.config_path)
def unregister():
@ -54,12 +56,11 @@ def unregister():
bpy.utils.previews.remove(ASSET_PREVIEWS)
except Exception as e:
pass
# print(f"!-- {e} --!")
# print(f"No preview collection found. {ASSET_PREVIEWS} can't be remove.")
bpy.app.handlers.frame_change_post.remove(set_active_strip)
for module in reversed(modules):
module.unregister()
if __name__ == "__main__":
register()

View File

@ -25,6 +25,9 @@ PROJECT_ITEMS = []
EPISODE_ITEMS = []
CONFIG_DIR = Path(appdirs.user_config_dir(__package__.split('.')[0]))
APP_TEMPLATES_DIR = Path(MODULE_DIR, 'resources', 'app_templates')
REVIEW_TEMPLATE_BLEND = Path(APP_TEMPLATES_DIR, 'Review', 'startup.blend')
PREVIEWS_DIR = CONFIG_DIR / 'thumbnails'
TASK_ITEMS = []

View File

@ -5,6 +5,9 @@ import platform
import sys
import unicodedata
from pathlib import Path
from os.path import expandvars
import json
import glob
def install_module(module_name, package_name=None):
@ -109,13 +112,15 @@ def read_file(path):
yaml = install_module('yaml')
try:
data = yaml.safe_load(txt)
except Exception:
except Exception as e:
print(e)
print(f'Could not load yaml file {path}')
return
elif path.suffix.lower() == '.json':
try:
data = json.loads(txt)
except Exception:
except Exception as e:
print(e)
print(f'Could not load json file {path}')
return
else:
@ -142,11 +147,36 @@ 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(string, template):
# 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()
def find_last(template, **kargs):
#print(find_last, template, kargs)
pattern = Path(template).as_posix()
pattern = expandvars(pattern)
for key, value in kargs.items():
pattern = re.sub(r'\{%s\}'%key, value, pattern)
pattern = re.sub(r'{\w+:(\d{2})d}', lambda x : '?' * int(x.groups()[0]), pattern)
pattern = re.sub(r'{.*?}', '*', pattern)
print(pattern)
filepaths = glob.glob(pattern)
if filepaths:
return Path(max(filepaths))

View File

@ -40,15 +40,26 @@ class VSETB_OT_load_settings(Operator):
def execute(self, context):
prefs = get_addon_prefs()
settings = get_scene_settings()
project = settings.active_project
if not prefs.config_path:
return {'FINISHED'}
return {'CANCELLED'}
addon_config = read_file(os.path.expandvars(prefs.config_path))
if not addon_config:
return {'CANCELLED'}
addon_config['trackers'] = addon_config.get('trackers')
trackers = addon_config.pop('trackers')
trackers_config = addon_config.pop('trackers')
for tracker_config in trackers_config:
tracker_name = norm_str(tracker_config.pop('name'))
if not hasattr(prefs.trackers, tracker_name):
continue
tracker = getattr(prefs.trackers, tracker_name)
for k, v in tracker_config.items():
setattr(tracker, k, v)
addon_config['spreadsheet_export'] = addon_config.get('spreadsheet_export', {})
spreadsheet_export_config = addon_config.pop('spreadsheet_export')
@ -57,10 +68,15 @@ class VSETB_OT_load_settings(Operator):
spreadsheet_import_config = addon_config.pop('spreadsheet_import')
project_name = addon_config.get('project_name')
if project_name:
if project_name not in settings.projects:
project = settings.projects.add()
project.name = project_name
settings.project_name = project_name
#if project_name and project_name in settings.projects:
# settings.project_name = project_name
# Project Properties
project = settings.active_project
for k, v in addon_config.items():
try:
setattr(project, k, v)
@ -132,6 +148,7 @@ class VSETB_OT_load_settings(Operator):
except Exception:
print(f'Could not set option {k} with value {v} to spreadsheet')
self.report({"INFO"}, 'Settings loaded with sucess')
return {'FINISHED'}

View File

@ -134,7 +134,7 @@ class VSETB_OT_render(Operator):
if project.render_sequence:
print('Render Sequences...')
for strip in get_strips(channel='Sequences', selected_only=project.render_selected_only):
print(strip.name)
#print(strip.name)
strip_settings = strip.vsetb_strip_settings
strip_data = {**format_data, **strip_settings.format_data}

View File

@ -1,5 +1,9 @@
from pathlib import Path
from os.path import expandvars
import re
import glob
from tempfile import gettempdir
import bpy
from bpy.types import Operator, UIList
@ -9,11 +13,11 @@ from vse_toolbox.constants import (EDITS, EDIT_SUFFIXES, MOVIES, MOVIE_SUFFIXES,
SOUNDS, SOUND_SUFFIXES)
from vse_toolbox.sequencer_utils import (clean_sequencer, import_edit, import_movie,
import_sound, get_strips, get_channel_index)
import_sound, get_strips, get_channel_index, get_empty_channel, scale_clip_to_fit)
from vse_toolbox.bl_utils import get_scene_settings
from vse_toolbox.file_utils import install_module
from vse_toolbox.bl_utils import (get_scene_settings, get_addon_prefs, get_scene_settings)
from vse_toolbox.file_utils import install_module, parse, find_last
#from vse_toolbox.template import Template
class VSETB_OT_auto_select_files(Operator):
bl_idname = "vse_toolbox.auto_select_files"
@ -194,15 +198,26 @@ class VSETB_UL_import_task(UIList):
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
layout.separator(factor=0.5)
layout.prop(item, 'import_enabled', text='')
layout.label(text=item.name)
class VSETB_OT_import_shots(Operator):
bl_idname = "vse_toolbox.import_shots"
bl_label = "Import Shots"
bl_description = "Import Shots for disk or tracker"
def get_task_items(self, context):
settings = get_scene_settings()
project = settings.active_project
return [(t, t, '') for t in project.task_types.keys()]
class VSETB_OT_select_task(Operator):
bl_idname = "vse_toolbox.select_task"
bl_label = "Select Task"
bl_description = "Select Task"
bl_options = {"REGISTER", "UNDO"}
bl_property = "task"
task: EnumProperty(name="My Search", items=get_task_items)
@classmethod
def poll(cls, context):
return True
@ -212,76 +227,369 @@ class VSETB_OT_import_shots(Operator):
settings = get_scene_settings()
project = settings.active_project
from gadget import Project
project.task_types[self.task].import_enabled = True
p = Project.from_env()
p.shots.fetch()
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class VSETB_OT_unselect_task(Operator):
bl_idname = "vse_toolbox.unselect_task"
bl_label = "Unselect Task"
bl_description = "Unselect Task"
bl_options = {"REGISTER", "UNDO"}
task : StringProperty()
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.task_types[self.task].import_enabled = False
return {'FINISHED'}
def get_sequence_items(self, context):
settings = get_scene_settings()
project = settings.active_project
return [(t, t, '') for t in project.sequences.keys()]
class VSETB_OT_select_sequence(Operator):
bl_idname = "vse_toolbox.select_sequence"
bl_label = "Select Sequence"
bl_description = "Select Sequence"
bl_options = {"REGISTER", "UNDO"}
bl_property = "sequence"
sequence: EnumProperty(name="My Search", items=get_sequence_items)
@classmethod
def poll(cls, context):
return True
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.sequences[self.sequence].import_enabled = True
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class VSETB_OT_unselect_sequence(Operator):
bl_idname = "vse_toolbox.unselect_sequence"
bl_label = "Unselect sequence"
bl_description = "Unselect sequence"
bl_options = {"REGISTER", "UNDO"}
sequence : StringProperty()
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
project.sequences[self.sequence].import_enabled = False
return {'FINISHED'}
class VSETB_OT_import_shots(Operator):
bl_idname = "vse_toolbox.import_shots"
bl_label = "Import Shots"
bl_description = "Import Shots for disk or tracker"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return get_scene_settings().active_project
def set_sequencer_channels(self, task_types):
scn = bpy.context.scene
channels = ['Shots', 'Sequences', 'Stamps']
#channel_index = len(task_types * 3) + len(channels) + 1
channel_index = 1
for task_type in task_types:
audio_channel = scn.sequence_editor.channels[channel_index]
audio_channel.name = f'{task_type} Audio'
audio_channel.mute = (task_type != task_types[-1])
video_channel = scn.sequence_editor.channels[channel_index+1]
video_channel.name = f'{task_type} Video'
tasks = [t for t in project.task_types if t.import_enabled]
for i, task_type in enumerate(tasks):
channel_index = 9 + i
channel_index += 3
scn.sequence_editor.channels[channel_index].name = task_type.name
for channel in channels:
scn.sequence_editor.channels[channel_index].name = channel
channel_index += 1
# Create Channels:
scn.sequence_editor.channels[9]
def get_preview_dir(self):
preview_dir = Path(bpy.app.tempdir, 'previews')
if bpy.data.filepath:
preview_dir = Path(bpy.data.filepath).parent / 'previews'
return preview_dir
for strip in get_strips('Shots'):
for i, task_type in enumerate(tasks):
shot = p.shots[strip.name]
versions = shot.tasks[task_type.name].outputs[0].versions.fetch()
def download_preview(self, task_type, shot):
prefs = get_addon_prefs()
tracker = prefs.tracker
if versions:
print(versions[-1])
imported_strip = import_movie(versions[-1].path)
imported_strip.frame_start = strip.frame_start
task = tracker.get_task(task_type.id or task_type.name, entity=shot)
last_comment = tracker.get_last_comment_with_preview(task)
if not last_comment:
return
if imported_strip.frame_final_duration != strip.frame_final_duration:
print(f'strip {strip.name} has different duration')
imported_strip.color_tag = 'COLOR_03'
shot_name = shot['name']
sequence_name = f"{shot['sequence_name']}_"
if shot_name.startswith(sequence_name):
shot_name = shot_name.replace(sequence_name, '', 1)
preview_dir = self.get_preview_dir()
filepath = Path(preview_dir, f'{sequence_name}{shot_name}_{task_type.name}')
filepath.parent.mkdir(parents=True, exist_ok=True)
last_preview = last_comment['previews'][0]
tracker.download_preview_file(last_preview, str(filepath))
return filepath
def conform_render(self, clip):
scn = bpy.context.scene
scn.render.resolution_x = clip.elements[0].orig_width
scn.render.resolution_y = clip.elements[0].orig_height
scn.render.fps = int(clip.elements[0].orig_fps)
scn.view_settings.view_transform = 'Standard'
def find_shot_preview(self, sequence, shot_name, task_type):
settings = get_scene_settings()
project = settings.active_project
import_shots = project.import_shots
pattern = Path(
import_shots.shot_folder_template,
import_shots.import_video_template)
# Normalize name in snake_case for now
sequence = sequence.name.lower().replace(' ', '_')
shot = parse(project.shot_template, shot_name)['shot']
task = task_type.name.lower().replace(' ', '_')
return find_last(pattern, sequence=sequence, shot=shot, task=task)
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
import_shots = project.import_shots
prefs = get_addon_prefs()
tracker = prefs.tracker
tracker.admin_connect()
task_types = [t for t in project.task_types if t.import_enabled]
sequences = [s for s in project.sequences if s.import_enabled]
conformed = False
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 = int(shot_data['nb_frames'])
strip = scn.sequence_editor.sequences.new_effect(
name=shot_data['name'],
type='COLOR',
channel=get_channel_index('Shots'),
frame_start=frame_index,
frame_end=frame_index + frames
)
strip.blend_alpha = 0
strip.color = (0.5, 0.5, 0.5)
for task_type in task_types:
if import_shots.import_source == 'DISK':
preview = self.find_shot_preview(sequence, shot_data['name'], task_type)
#print(videos)
#return {"FINISHED"}
imported_strip.frame_final_end = strip.frame_final_end
else:
preview = self.download_preview(task_type, shot_data)
imported_strip.channel = get_channel_index(task_type.name)
print(get_channel_index(task_type.name), imported_strip.channel)
else:
print(f'No movie for {strip.name} of task {task_type.name}')
if not preview:
print(f'No preview found for shot {shot_data["name"]}')
continue
print(f'Loading Preview from {preview}')
# Load Video
channel_index = get_channel_index(f'{task_type.name} Video')
video_clip = import_movie(preview)
video_clip.frame_start = frame_index
video_clip.channel = channel_index
# for task_type in tasks:
# for strip in get_strips(task_type.name):
# strip.channel = get_channel_index(task_type.name)
if not conformed:
self.conform_render(video_clip)
conformed = True
scale_clip_to_fit(video_clip)
# Load Audio
channel_index = get_channel_index(f'{task_type.name} Audio')
audio_clip = import_sound(preview, frame_start=frame_index,
frame_end=video_clip.frame_final_end)
audio_clip.channel = channel_index
frame_index += frames
strip = scn.sequence_editor.sequences.new_effect(
name=sequence.name,
type='COLOR',
channel=get_channel_index('Sequences'),
frame_start=sequence_start,
frame_end=frame_index
)
strip.blend_alpha = 0
strip.color = (0.25, 0.25, 0.25)
scn.frame_start = 1
scn.frame_end = frame_index -1
return {'FINISHED'}
def draw(self, context):
settings = get_scene_settings()
project = settings.active_project
import_shots = project.import_shots
layout = self.layout
col = layout.column(align=False)
col.use_property_split = True
col.use_property_decorate = False
row = col.row(align=True)
row.prop(project, "import_source", text='Source', expand=True)
row.prop(import_shots, "import_source", text='Videos Source', expand=True)
if import_shots.import_source == 'DISK':
#col.prop(import_shots, "sequence_dir_template", text='Sequence Dir')
col.prop(import_shots, "shot_folder_template", text='Shot Folder')
col.prop(import_shots, "import_video_template", text='Video')
col.separator()
else:
col.prop(import_shots, "previews_folder", text='Previews Folder')
col.separator()
#if bpy.data.filepath:
#col.label(text=f' {self.get_preview_dir()}')
#else:
# col.label(icon="ERROR", text="Save your Blender to keep the previews")
row = col.row(align=True)
row.prop(project, 'import_task', text='Import Tasks')
row.prop(import_shots, 'import_task', text='Import Tasks')
row.operator("vse_toolbox.select_task", text='', icon='ADD')
if project.import_task == 'SELECTED':
col.use_property_split = False
col.template_list("VSETB_UL_import_task", "import_task", project, "task_types", project, "task_type_index", rows=8)
tasks = [t for t in project.task_types if t.import_enabled]
if import_shots.import_task == "FROM_LIST":
if tasks:
split = col.split(factor=0.4)
split.row()
box = split.box()
box_col = box.column(align=True)
for task_type in tasks:
row = box_col.row(align=True)
sub = row.row(align=True)
sub.enabled = False
sub.alignment = 'LEFT'
sub.scale_x = 0.15
sub.prop(task_type, 'color', text='')
op = row.operator("vse_toolbox.unselect_task", text=task_type.name)
op.task = task_type.name
row.separator(factor=0.5)
op = row.operator("vse_toolbox.unselect_task", text='', icon='REMOVE', emboss=False)
op.task = task_type.name
else:
split = col.split(factor=0.4)
split.row()
row = split.row()
row.label(icon="ERROR")
row.label(text='Add at least one task')
# Choose Sequences
row = col.row(align=True)
row.prop(import_shots, 'import_sequence', text='Import Sequences')
row.operator("vse_toolbox.select_sequence", text='', icon='ADD')
sequences = [s for s in project.sequences if s.import_enabled]
if import_shots.import_sequence == "FROM_LIST":
if sequences:
split = col.split(factor=0.4)
split.row()
box = split.box()
box_col = box.column(align=True)
for sequence in sequences:
row = box_col.row(align=True)
op = row.operator("vse_toolbox.unselect_sequence", text=sequence.name)
op.sequence = sequence.name
row.separator(factor=0.5)
op = row.operator("vse_toolbox.unselect_sequence", text='', icon='REMOVE', emboss=False)
op.sequence = sequence.name
else:
split = col.split(factor=0.4)
split.row()
row = split.row()
row.label(icon="ERROR")
row.label(text='Add at least one Sequence')
def invoke(self, context, event):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
tmp_dir = Path(gettempdir(), 'reviews')
if bpy.data.filepath and Path(project.import_shots.previews_folder) == tmp_dir:
tmp_dir = '//sources'
if not bpy.data.filepath and project.import_shots.previews_folder == '//sources':
project.import_shots.previews_folder = str(tmp_dir)
# if not bpy.data.filepath:
# self.report({"ERROR"}, "Save your Blender file first")
# return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self)
return context.window_manager.invoke_props_dialog(self, width=400)
def check(self, context):
return True
classes = (
VSETB_OT_select_sequence,
VSETB_OT_unselect_sequence,
VSETB_OT_unselect_task,
VSETB_OT_select_task,
VSETB_UL_import_task,
VSETB_OT_auto_select_files,
VSETB_OT_import_files,

View File

@ -1,4 +1,5 @@
from os.path import expandvars
from os.path import expandvars, abspath
from pathlib import Path
import bpy
from bpy.types import Operator
from bpy.props import (BoolProperty, StringProperty)
@ -7,6 +8,8 @@ from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels
get_channel_index, new_text_strip, get_strip_at, get_channel_name)
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings
from vse_toolbox.constants import REVIEW_TEMPLATE_BLEND
from shutil import copy2
class VSETB_OT_rename(Operator):
@ -17,8 +20,7 @@ class VSETB_OT_rename(Operator):
#template : StringProperty(name="Strip Name", default="")
#increment : IntProperty(name="Increment", default=0)
channel_name : StringProperty(name="Channel Name", default="")
selected_only : BoolProperty(name="Selected Only", default=False)
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",
@ -29,7 +31,8 @@ class VSETB_OT_rename(Operator):
@classmethod
def poll(cls, context):
settings = get_scene_settings()
return settings.active_project
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
@ -43,39 +46,62 @@ class VSETB_OT_rename(Operator):
settings = get_scene_settings()
project = settings.active_project
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_sequence_strip
channel_name = get_channel_name(strip)
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
if self.channel_name == 'Shots':
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')
col.prop(project, 'shot_padding', text='Padding')
col.prop(project, 'reset_by_sequence')
elif self.channel_name == 'Sequences':
elif channel_name == 'Sequences':
col.prop(project, 'sequence_template' ,text='Sequence Name')
col.prop(project, 'sequence_start_number', text='Start Number')
col.prop(project, 'sequence_increment', text='Increment')
col.prop(project, 'sequence_padding', text='Padding')
col.prop(self, 'selected_only')
if channel_name == 'Shots':
label = project.shot_template.format(episode=episode, sequence=sequence, shot=shot)
elif channel_name == 'Sequences':
label = project.sequence_template.format(episode=episode, sequence=sequence)
col.label(text=f'Renaming {label}')
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
strips = get_strips(channel=self.channel_name, selected_only=self.selected_only)
if self.channel_name == 'Shots':
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 == 'Shots':
rename_strips(strips,
template=project.shot_template,
increment=project.shot_increment, start_number=project.shot_start_number,
by_sequence=project.reset_by_sequence
by_sequence=project.reset_by_sequence,
padding=project.shot_padding
)
if self.channel_name == 'Sequences':
if channel_name == 'Sequences':
rename_strips(strips,
template=project.sequence_template,
increment=project.sequence_increment, start_number=project.sequence_start_number
increment=project.sequence_increment, start_number=project.sequence_start_number,
padding=project.sequence_padding
)
return {"FINISHED"}
@ -180,10 +206,10 @@ class VSETB_OT_set_stamps(Operator):
font_size=font_size, y=margin, box_margin=box_margin, select=True, box_color=(0, 0, 0, 0.5))
# Project Name
project_text = '{project_name}'
project_text = '{project}'
if project.type == 'TVSHOW':
project_text = '{project_name} / {episode_name}'
project_strip_stamp = new_text_strip('project_name_stamp', channel=1, **stamp_params,
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
@ -191,16 +217,16 @@ class VSETB_OT_set_stamps(Operator):
# Shot Name
shot_strip_stamp = new_text_strip('shot_name_stamp', channel=2, **stamp_params,
text='{sequence_name} / {shot_name}', align_y='BOTTOM')
shot_strip_stamp = new_text_strip('shot_stamp', channel=2, **stamp_params,
text='sq{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 Range
frame_strip_stamp = new_text_strip('frame_range_stamp', channel=3, **stamp_params,
text='{shot_frame} / {shot_duration}', x=0.99, align_x='RIGHT', align_y='BOTTOM')
# Frame
frame_strip_stamp = new_text_strip('frame_stamp', channel=3, **stamp_params,
text='{shot_frame} / {shot_duration} {timecode}', x=0.99, align_x='RIGHT', align_y='BOTTOM')
frame_strip_stamp.crop.min_x = crop_x *2
frame_strip_stamp.crop.max_y = crop_max_y
@ -269,10 +295,11 @@ class VSETB_OT_next_shot(Operator):
return {"FINISHED"}
class VSETB_OT_open_shot_dir(Operator):
bl_idname = "vse_toolbox.open_shot_dir"
bl_label = "Open Shot Dir"
bl_description = "Open Shot Dir"
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
@ -289,13 +316,154 @@ class VSETB_OT_open_shot_dir(Operator):
format_data = {**settings.format_data, **project.format_data, **strip_settings.format_data}
shot_dir_template = expandvars(project.shot_dir_template)
shot_dir_path = shot_dir_template.format(**format_data)
bpy.ops.wm.path_open(filepath=shot_dir_path)
shot_folder_template = expandvars(project.import_shots.shot_folder_template)
shot_folder_path = shot_folder_template.format(**format_data)
bpy.ops.wm.path_open(filepath=shot_folder_path)
return {"FINISHED"}
class VSETB_OT_collect_files(Operator):
bl_idname = "vse_toolbox.collect_files"
bl_label = "Collect Files"
bl_description = "Collect Files"
bl_options = {"REGISTER", "UNDO"}
collect_folder: StringProperty(
name="Collect Folder", default="//sources", subtype='DIR_PATH')
@classmethod
def poll(cls, context):
if bpy.data.is_saved:
return True
cls.poll_message_set('Save the blend to collect files')
def execute(self, context):
strip = context.active_sequence_strip
settings = get_scene_settings()
project = settings.active_project
strips = [s for s in context.scene.sequence_editor.sequences_all if s.type in ('MOVIE', 'SOUND')]
context.window_manager.progress_begin(0, len(strips))
for i, strip in enumerate(strips):
context.window_manager.progress_update(i)
if strip.type == 'MOVIE':
src_path = strip.filepath
elif strip.type == 'SOUND':
src_path = strip.sound.filepath
src_path = Path(abspath(bpy.path.abspath(src_path)))
dst_path = Path(bpy.path.abspath(self.collect_folder), src_path.name)
if src_path == dst_path:
continue
dst_path.parent.mkdir(exist_ok=True, parents=True)
print(f'Copy file from {src_path} to {dst_path}')
copy2(str(src_path), str(dst_path))
rel_path = bpy.path.relpath(str(dst_path))
if len(Path(rel_path).as_posix()) < len(dst_path.as_posix()):
dst_path = rel_path
if strip.type == 'MOVIE':
strip.filepath = str(dst_path)
elif strip.type == 'SOUND':
strip.sound.filepath = str(dst_path)
context.window_manager.progress_end()
return {"FINISHED"}
def invoke(self, context, event):
scn = context.scene
settings = get_scene_settings()
return context.window_manager.invoke_props_dialog(self)
class WM_OT_split_view(Operator):
"""Toggle Split sequencer view"""
bl_idname = "wm.split_view"
bl_label = "Split View"
def get_preview_areas(self, context):
return [
x for x in context.screen.areas
if x.type == "SEQUENCE_EDITOR" and x.spaces[0].view_type == "PREVIEW"
]
def invoke(self, context, event):
preview_areas = self.get_preview_areas(context)
if not preview_areas:
return {"CANCELLED"}
scn = context.scene
video_channels = [i for i, c in enumerate(scn.sequence_editor.channels) if 'Video' in c.name]
if len(video_channels) == 2 and len(preview_areas) == 1:
# Split area
with bpy.context.temp_override(area=preview_areas[0]):
bpy.ops.screen.area_split(direction="VERTICAL")
# Disable toolbar on right panel
# Update areas
preview_areas = self.get_preview_areas(context)
preview_areas[-1].spaces[0].display_channel = video_channels[-1]
preview_areas[0].spaces[0].display_channel = video_channels[-2]
preview_areas[0].spaces[0].show_gizmo_navigate = False
preview_areas[-1].spaces[0].show_gizmo_navigate = False
# Hide toolbar
#preview_areas[0].spaces[0].show_region_toolbar = False
else:
# Give the remaining area to have the left area's channel displayed
# Show toolbar of remaning area
#preview_areas[0].spaces[0].show_region_toolbar = True
# Join areas
# Simulates the mouse position as being between the two areas horizontally
# and a bit above the bottom corner vertically
cursor_x = int(preview_areas[0].x - (preview_areas[0].x - preview_areas[1].width) / 2)
cursor_y = preview_areas[0].y + 10
bpy.ops.screen.area_join(cursor=(cursor_x, cursor_y))
preview_areas = self.get_preview_areas(context)
preview_areas[0].spaces[0].display_channel = 0
# Force UI update, due to Blender bug, delete when fixed -> https://developer.blender.org/T65529
bpy.ops.screen.area_swap(cursor=(preview_areas[0].x, preview_areas[0].y))
bpy.ops.screen.area_swap(cursor=(preview_areas[0].x, preview_areas[0].y))
return {"FINISHED"}
'''
class VSETB_OT_set_review_workspace(Operator):
"""Set Review Workspace"""
bl_idname = "vse_toolbox.set_workspace"
bl_label = "Set Review Workspace"
def execute(self, context):
bpy.ops.workspace.append_activate(idname='Review', filepath=str(REVIEW_TEMPLATE_BLEND))
return {"FINISHED"}
'''
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
@ -331,7 +499,9 @@ classes = (
VSETB_OT_show_waveform,
VSETB_OT_previous_shot,
VSETB_OT_next_shot,
VSETB_OT_open_shot_dir
VSETB_OT_open_shot_folder,
VSETB_OT_collect_files,
WM_OT_split_view
)
def register():

View File

@ -3,6 +3,8 @@ import os
from os.path import expandvars
from pathlib import Path
from pprint import pprint
import webbrowser
from functools import partial
import bpy
from bpy.types import (Operator, )
@ -59,7 +61,7 @@ class VSETB_OT_load_assets(Operator):
prefs = get_addon_prefs()
tracker = prefs.tracker
tracker.connect()
tracker.admin_connect()
project = settings.active_project
project.assets.clear()
@ -125,7 +127,7 @@ class VSETB_OT_load_projects(Operator):
tracker = prefs.tracker
prev_project_name = settings.project_name
tracker.connect()
tracker.admin_connect()
project_datas = tracker.get_projects()
for project_data in sorted(project_datas, key=lambda x: x['name']):
@ -137,7 +139,6 @@ class VSETB_OT_load_projects(Operator):
project.name = project_data['name']
project.id = project_data['id']
if project.type == 'TVSHOW':
episode_datas = tracker.get_episodes(project_data)
for episode_data in episode_datas:
@ -154,6 +155,17 @@ 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))
else:
# Add sequences
sequences_data = tracker.get_sequences(project_data)
for sequence_data in sequences_data:
sequence = project.sequences.get(sequence_data['name'])
if not sequence:
sequence = project.sequences.add()
sequence.name = sequence_data['name']
sequence.id = sequence_data['id']
project.metadata_types.clear()
for metadata_data in tracker.get_metadata_types(project_data):
@ -183,6 +195,7 @@ class VSETB_OT_load_projects(Operator):
for task_type_data in tracker.get_shot_task_types(project_data):
task_type = project.task_types.add()
task_type.name = task_type_data['name']
task_type.id = task_type_data['id']
task_type.color = self.hex_to_rgb(task_type_data['color'])[:3]
project.set_shot_tasks()
@ -200,11 +213,11 @@ class VSETB_OT_load_projects(Operator):
if project.name not in project_names:
settings.projects.remove(list(settings.projects).index(project))
# Restore previous project
settings.project_name = prev_project_name
bpy.ops.vse_toolbox.load_settings()
if prev_project_name != '/' and prev_project_name in settings.projects:
settings.project_name = prev_project_name
#if settings.active_project:
# settings.active_project.set_strip_metadata()
@ -216,7 +229,7 @@ class VSETB_OT_load_projects(Operator):
class VSETB_OT_new_episode(Operator):
bl_idname = "vse_toolbox.new_episode"
bl_label = "New Epispde"
bl_label = "New Episode"
bl_description = "Add new Episode to Project"
bl_options = {"REGISTER", "UNDO"}
@ -254,42 +267,12 @@ class VSETB_OT_new_episode(Operator):
return {'FINISHED'}
def get_task_status_items(self, context):
settings = get_scene_settings()
project = settings.active_project
status_items = [('CURRENT', 'Current', '')]
if project:
status_items += [(t.name, t.name, '') for t in project.task_statuses]
return status_items
def get_task_type_items(self, context):
settings = get_scene_settings()
project = settings.active_project
if not project:
return [('NONE', 'None', '')]
return [(t.name, t.name, '') for t in project.task_types]
class VSETB_OT_upload_to_tracker(Operator):
bl_idname = "vse_toolbox.upload_to_tracker"
bl_label = "Upload to tracker"
bl_description = "Upload selected strip to tracker"
bl_options = {"REGISTER", "UNDO"}
task : EnumProperty(items=get_task_type_items)
status : EnumProperty(items=get_task_status_items)
comment : StringProperty()
add_preview : BoolProperty(default=True)
preview_mode : EnumProperty(items=[(m, m.title().replace('_', ' '), '')
for m in ('ONLY_NEW', 'REPLACE', 'ADD')])
set_main_preview : BoolProperty(default=True)
casting : BoolProperty(default=True)
custom_data : BoolProperty(default=True)
tasks_comment: BoolProperty(default=True)
@classmethod
def poll(cls, context):
return True
@ -297,49 +280,66 @@ class VSETB_OT_upload_to_tracker(Operator):
def invoke(self, context, event):
prefs = get_addon_prefs()
settings = get_scene_settings()
project = settings.active_project
self.project = settings.active_project
tracker = prefs.tracker
tracker.connect()
#self.bl_label = f"Upload to {settings.tracker_name.title()}"
return context.window_manager.invoke_props_dialog(self)
return context.window_manager.invoke_props_dialog(self, width=350)
def draw(self, context):
scn = context.scene
settings = get_scene_settings()
upload_to_tracker = self.project.upload_to_tracker
layout = self.layout
col = layout.column()
col.use_property_split = True
col.prop(self, 'task', text='Task')
col.prop(self, 'status', text='Status')
col.prop(self, 'comment', text='Comment')
row = col.row(heading='Add Preview')
row.prop(self, 'add_preview', text='')
row.prop(self, 'preview_mode', text='')
col.use_property_decorate = False
col.prop(upload_to_tracker, 'render_strips', text='Render Strips')
if upload_to_tracker.render_strips:
col.use_property_split = False
col.prop(upload_to_tracker, 'render_strip_template', text='')
col.use_property_split = True
#else:
# col.label(text=f'Source: {self.project.render_video_strip_template}')
col.separator()
col.prop(self, 'casting', text='Casting')
col.prop(self, 'custom_data', text='Custom Data')
col.prop(self, 'tasks_comment', text='Tasks Comment')
col.prop(self, 'set_main_preview', text='Set Main Preview')
col.prop(upload_to_tracker, 'task', text='Task')
col.prop(upload_to_tracker, 'status', text='Status')
col.prop(upload_to_tracker, 'comment', text='Comment')
row = col.row(heading='Add Preview')
row.prop(upload_to_tracker, 'add_preview', text='')
row.prop(upload_to_tracker, 'preview_mode', text='')
if upload_to_tracker.add_preview and not upload_to_tracker.render_strips:
col.use_property_split = False
col.prop(self.project, 'render_video_strip_template', text='')
col.use_property_split = True
col.separator()
col.prop(upload_to_tracker, 'custom_data', text='Custom Data')
col.prop(upload_to_tracker, 'update_frames', text='Update frames')
col.prop(upload_to_tracker, 'casting', text='Casting')
col.prop(upload_to_tracker, 'tasks_comment', text='Tasks Comment')
col.prop(upload_to_tracker, 'set_main_preview', text='Set Main Preview')
def execute(self, context):
#self.report({'ERROR'}, f'Export not implemented yet.')
prefs = get_addon_prefs()
settings = get_scene_settings()
project = settings.active_project
upload_to_tracker = self.project.upload_to_tracker
episode = None
if settings.active_episode:
episode = settings.active_episode.id
format_data = {**settings.format_data, **project.format_data}
format_data = {**settings.format_data, **self.project.format_data}
tracker = prefs.tracker
status = self.status
status = upload_to_tracker.status
if status == 'CURRENT':
status = None
@ -360,15 +360,19 @@ class VSETB_OT_upload_to_tracker(Operator):
self.report({"INFO"}, f'Create shot {shot_name} in Kitsu')
shot = tracker.new_shot(shot_name, sequence=sequence)
task = tracker.get_task(self.task, entity=shot)
task = tracker.get_task(upload_to_tracker.task, entity=shot)
if not task:
task = tracker.new_task(shot, task_type=self.task)
task = tracker.new_task(shot, task_type=upload_to_tracker.task)
preview = None
if self.add_preview:
if upload_to_tracker.add_preview:
strip_data = {**format_data, **strip_settings.format_data}
preview_template = expandvars(project.render_video_strip_template)
if upload_to_tracker.render_strips:
preview_template = expandvars(upload_to_tracker.render_strip_template)
else:
preview_template = expandvars(self.project.render_video_strip_template)
preview = preview_template.format(**strip_data)
preview = Path(os.path.abspath(bpy.path.abspath(preview)))
#print(preview)
@ -377,24 +381,41 @@ class VSETB_OT_upload_to_tracker(Operator):
preview = None
elif task.get('last_comment') and task['last_comment']['previews']:
if self.preview_mode == 'REPLACE':
if upload_to_tracker.preview_mode == 'REPLACE':
tracker.remove_comment(task['last_comment'])
elif self.preview_mode == 'ONLY_NEW':
elif upload_to_tracker.preview_mode == 'ONLY_NEW':
preview = None
if status or preview:
tracker.new_comment(task, comment=self.comment, status=status, preview=preview, set_main_preview=self.set_main_preview)
comment_data = None
if status or comment or preview:
comment_data = tracker.new_comment(task, comment=upload_to_tracker.comment, status=status)
if preview:
print('Upload preview from', preview)
preview_data = tracker.new_preview(
task=task,
comment=comment_data,
preview=preview)
if self.custom_data:
description = strip_settings.description
tracker.update_data(shot, metadata, frames=strip.frame_final_duration, description=description)
if upload_to_tracker.set_main_preview:
bpy.app.timers.register(partial(tracker.set_main_preview, preview_data), first_interval=10)
params = {}
if upload_to_tracker.custom_data:
params['custom_data'] = metadata
params['description'] = strip_settings.description
if self.casting:
if upload_to_tracker.update_frames:
params['frames'] = strip.frame_final_duration
if params:
tracker.update_data(shot, **params)
if upload_to_tracker.casting:
casting = [{'asset_id': a.id, 'nb_occurences': a.instance} for a in strip_settings.casting]
tracker.update_casting(shot, casting)
if self.tasks_comment:
for task_type in project.task_types:
if upload_to_tracker.tasks_comment:
for task_type in self.project.task_types:
task = getattr(strip_settings.tasks, norm_name(task_type.name))
tracker_task = tracker.get_task(task_type.name, entity=shot)
@ -405,6 +426,28 @@ class VSETB_OT_upload_to_tracker(Operator):
return {"FINISHED"}
class VSETB_OT_open_shot_on_tracker(Operator):
bl_idname = "vse_toolbox.open_shot_on_tracker"
bl_label = "Open Shot on Tracker"
bl_description = "Open Shot on Tracker"
@classmethod
def poll(cls, context):
prefs = get_addon_prefs()
if prefs.tracker:
return True
def execute(self, context):
prefs = get_addon_prefs()
tracker = prefs.tracker
strips = get_strips(channel='Shots', selected_only=True)
url = tracker.get_shots_search_url([s.name for s in strips])
webbrowser.open_new_tab(url)
return {"FINISHED"}
@persistent
def set_asset_items(scene=None):
ASSET_ITEMS.clear()
@ -420,6 +463,7 @@ classes = (
VSETB_OT_new_episode,
VSETB_OT_tracker_connect,
VSETB_OT_upload_to_tracker,
VSETB_OT_open_shot_on_tracker
)
def register():

View File

@ -0,0 +1,45 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.app.handlers import persistent
def update_factory_startup_screens():
screen = bpy.data.screens["Review"]
def update_factory_startup_ffmpeg_preset():
from bpy import context
preset = "H264_in_MP4"
preset_filepath = bpy.utils.preset_find(preset, preset_path="ffmpeg")
if not preset_filepath:
print("Preset %r not found" % preset)
for scene in bpy.data.scenes:
render = scene.render
render.image_settings.file_format = 'FFMPEG'
if preset_filepath:
with context.temp_override(scene=scene):
bpy.ops.script.python_file_run(filepath=preset_filepath)
render.ffmpeg.audio_codec = 'AAC'
render.ffmpeg.audio_bitrate = 128
render.ffmpeg.audio_mixrate = 44100
@persistent
def load_handler(_):
update_factory_startup_screens()
if bpy.app.build_options.codec_ffmpeg:
update_factory_startup_ffmpeg_preset()
def register():
bpy.app.handlers.load_factory_startup_post.append(load_handler)
def unregister():
bpy.app.handlers.load_factory_startup_post.remove(load_handler)

Binary file not shown.

View File

@ -1,6 +1,7 @@
import bpy
import os
from os.path import expandvars
import re
import urllib3
import traceback
@ -26,20 +27,36 @@ class Kitsu(Tracker):
url: StringProperty()
login: StringProperty()
password: StringProperty(subtype='PASSWORD')
admin_login : StringProperty()
admin_password : StringProperty(subtype='PASSWORD')
def connect(self, url=None, login=None, password=None):
def admin_connect(self):
url = self.url
if not url.endswith('/api'):
url += '/api'
login = expandvars(self.admin_login or self.login)
password = expandvars(self.admin_password or self.password)
try:
res = gazu.log_in(login, password)
LOGIN = login
return res['user']
except Exception as e:
print(e)
def connect(self):
'''Connect to kitsu api using provided url, login and password'''
global LOGIN
urllib3.disable_warnings()
if url is None:
url = self.url
if login is None:
login = self.login
if password is None:
password = self.password
url = expandvars(self.url)
login = expandvars(self.login)
password = expandvars(self.password)
if not url:
print(f'Kitsu Url: {self.url} is empty')
@ -168,6 +185,16 @@ class Kitsu(Tracker):
return gazu.shot.get_sequence_by_name(**params)
def get_sequences(self, project=None, episode=None):
#print(f'get_sequence({sequence=}, {project=})')
project = self.get_project(project)
if episode:
episode = self.get_episode(episode)
return gazu.shot.all_sequences_for_episode(episode)
else:
return gazu.shot.all_sequences_for_project(project)
def get_shot(self, shot, sequence, project=None):
#print(f'get_shot({shot=}, {sequence=}, {project=})')
project = self.get_project(project)
@ -182,6 +209,12 @@ class Kitsu(Tracker):
return gazu.shot.get_shot_by_name(sequence, shot)
def get_shots(self, sequence):
#print(f'get_sequence({sequence=}, {project=})')
#project = self.get_project(project)
return gazu.shot.all_shots_for_sequence(sequence)
def get_asset(self, asset, asset_type=None, project=None):
#print('get_asset', "name", name, 'asset_type', asset_type)
@ -211,6 +244,29 @@ class Kitsu(Tracker):
task = self.get_id(task)
return gazu.task.get_last_comment_for_task(task)
def get_last_comment_with_preview(self, task):
task = self.get_id(task)
comments = gazu.task.all_comments_for_task(task)
for comment in comments:
if comment['previews']:
return comment
def download_preview_file(self, preview, filepath):
preview_id = self.get_id(preview)
return gazu.files.download_preview_file(preview_id, str(filepath))
def get_shots_search_url(self, shot_names, project=None, episode=None):
project = self.get_project(project)
url = gazu.client.get_host().replace('/api', f'/productions/{project}')
if episode:
episode = self.get_episode(episode)
url += f'/episodes/{episode["id"]}'
url += f'/shots'
return f'{url}?search={" ".join(shot_names)}'
def get_task(self, task=None, entity=None):
entity = self.get_id(entity)
@ -225,6 +281,9 @@ class Kitsu(Tracker):
return task
def set_main_preview(self, preview_data):
gazu.task.set_main_preview(preview_data)
def new_preview(self, task, comment, preview, set_main_preview=False):
#print('new_preview', task, comment, preview, set_main_preview)
@ -238,7 +297,7 @@ class Kitsu(Tracker):
if set_main_preview:
#print('----', 'Settings')
gazu.task.set_main_preview(preview_data)
self.set_main_preview(preview_data)
return preview_data
@ -327,16 +386,13 @@ class Kitsu(Tracker):
return shot
def update_data(self, entity, data, name=None, description=None, frames=None, clear=False):
def update_data(self, entity, custom_data={}, name=None, description=None, frames=None, clear=False):
if isinstance(entity, dict):
entity_id = entity['id']
else:
entity_id = self.get_id(entity)
entity = gazu.client.fetch_one('entities', entity_id)
if data.get('custom_data'):
data['data'] = data.pop('custom_data')
if name:
entity['name'] = name
if description:
@ -345,9 +401,9 @@ class Kitsu(Tracker):
entity['nb_frames'] = frames
if clear or not entity['data']:
entity['data'] = data
entity['data'] = custom_data
else:
entity['data'].update(data)
entity['data'].update(custom_data)
#print('######UPDATE DATA')
#pprint(entity)
@ -387,6 +443,11 @@ class Kitsu(Tracker):
return gazu.casting.update_shot_casting(project, shot_id, norm_casting)
def draw_prefs(self, layout):
layout.prop(self, 'url', text='Url')
layout.prop(self, 'login', text='Login')
layout.prop(self, 'password', text='Password')
col = layout.column(align=False)
col.prop(self, 'url', text='Url')
col.prop(self, 'login', text='Login')
col.prop(self, 'password', text='Password')
col.separator()
col.prop(self, 'admin_login', text='Admin Login')
col.prop(self, 'admin_password', text='Admin Password')

View File

@ -13,6 +13,7 @@ from vse_toolbox.constants import SOUND_SUFFIXES
#import multiprocessing
#from multiprocessing.pool import ThreadPool
import subprocess
from collections import defaultdict
def frame_to_timecode(frame, fps):
@ -95,8 +96,7 @@ def get_strip_sequence_name(strip):
else:
return 'NoSequence'
def rename_strips(
strips, template, increment=10, start_number=0, by_sequence=False):
def rename_strips(strips, template, increment=10, start_number=0, padding=3, by_sequence=False):
scn = bpy.context.scene
settings = get_scene_settings()
@ -110,16 +110,20 @@ def rename_strips(
for strip in strips:
sequence_name = get_strip_sequence_name(strip)
sequence_data = parse(project.sequence_template, sequence_name)
if (by_sequence and prev_sequence_name and
sequence_name and sequence_name != prev_sequence_name):
strip_number = 0
name = template.format(
sequence=sequence_name,
format_data = dict(
sequence_strip=sequence_name,
episode=episode_name,
index=strip_number*increment+start_number
)
shot=str(strip_number*increment + start_number).zfill(padding))
format_data.update(sequence_data)
name = template.format(**format_data)
existing_strip = scn.sequence_editor.sequences_all.get(name)
if existing_strip:
@ -399,8 +403,13 @@ def import_movie(filepath):
res_x = scn.render.resolution_x
res_y = scn.render.resolution_y
if bpy.data.is_saved:
relpath = Path(bpy.path.relpath(str(filepath)))
if len(relpath.as_posix()) < len(filepath.as_posix()):
filepath = relpath
strip = scn.sequence_editor.sequences.new_movie(
name=filepath.stem,
name=Path(filepath).stem,
filepath=str(filepath),
channel=get_channel_index('Movie'),
frame_start=scn.frame_start
@ -414,28 +423,48 @@ def import_movie(filepath):
if src_height != res_y:
strip.transform.scale_y = (res_y / src_height)
if bpy.data.is_saved:
strip.filepath = bpy.path.relpath(str(filepath))
return strip
def import_sound(filepath):
def scale_clip_to_fit(strip):
scn = bpy.context.scene
res = scn.render.resolution_x, scn.render.resolution_y
strip_res = strip.elements[0].orig_width, strip.elements[0].orig_height
#print(strip.name, strip_res, width, height)
strip.transform.scale_x = res[0] / strip_res[0]
strip.transform.scale_y = strip.transform.scale_x
def import_sound(filepath, frame_start=None, frame_end=None):
scn = bpy.context.scene
strip = scn.sequence_editor.sequences.new_sound(
name=filepath.stem,
filepath=str(filepath),
channel=get_channel_index('Audio'),
frame_start=scn.frame_start
)
if frame_start is None:
frame_start = scn.frame_start
if bpy.data.is_saved:
strip.sound.filepath = bpy.path.relpath(str(filepath))
relpath = Path(bpy.path.relpath(str(filepath)))
if len(relpath.as_posix()) < len(filepath.as_posix()):
filepath = relpath
strip.show_waveform = True if strip.frame_final_duration < 10000 else False
strip = scn.sequence_editor.sequences.new_sound(
name=f'{filepath.stem} Audio',
filepath=str(filepath),
channel=get_channel_index('Audio'),
frame_start=frame_start
)
if frame_end is not None:
strip.frame_final_end = frame_end
#strip.show_waveform = True if strip.frame_final_duration < 10000 else False
return strip
def get_empty_channel():
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
sequences = []
@ -470,44 +499,51 @@ def set_active_strip(scene):
@persistent
def update_text_strips(scene):
if not scene.sequence_editor or not scene.sequence_editor.sequences_all:
return
#print("update_text_strips")
settings = get_scene_settings()
project = settings.active_project
episode = settings.active_episode
class MissingKey(dict):
def __missing__(self, key):
return '{' + key + '}'
scn = bpy.context.scene
if not scn.sequence_editor:
return
format_data = {
'scene': scene,
'project_name': 'None',
'episode_name': 'None',
'sequence_name': 'None',
'strip_name': 'None',
'shot_name': 'None',
'project': '...',
'episode': '...',
'sequence': '...',
'sequence_strip': '...',
'shot': '...',
'shot_strip': '...',
'shot_frame': 0,
'shot_duration': 0,
'shot_start': 0,
'shot_end': 0,
'time_code': ''
'timecode': ''
}
shot_strip = get_strip_at('Shots', frame=scene.frame_current)
if shot_strip:
format_data.update({
'project_name': project.name,
'episode_name': episode.name if episode else '',
'sequence_name': get_strip_sequence_name(shot_strip),
'strip_name': shot_strip.name,
'shot_name': 'sh{index:04d}'.format(**shot_strip.vsetb_strip_settings.format_data),
'project': project.name,
'episode': episode.name if episode else '',
'shot_duration': shot_strip.frame_final_duration,
'shot_frame': scene.frame_current - shot_strip.frame_final_start + 1,
'shot_start': shot_strip.frame_final_start,
'shot_end': shot_strip.frame_final_end,
'timecode' : frame_to_timecode(scene.frame_current, scene.render.fps)
})
format_data.update(shot_strip.vsetb_strip_settings.format_data)
for strip in scene.sequence_editor.sequences_all:
if not strip.type == 'TEXT':
@ -520,4 +556,4 @@ def update_text_strips(scene):
strip['text_pattern'] = strip.text
if 'text_pattern' in strip.keys():
strip.text = strip['text_pattern'].format(**format_data)
strip.text = strip['text_pattern'].format_map(MissingKey(**format_data))

View File

@ -7,7 +7,7 @@ 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.constants import ASSET_PREVIEWS
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.file_utils import norm_str
@ -20,27 +20,53 @@ class VSETB_main:
class VSETB_PT_main(VSETB_main, Panel):
bl_options = {"HIDE_HEADER"}
def draw_header_preset(self, context):
self.layout.operator('vse_toolbox.load_projects', icon='FILE_REFRESH', text='', emboss=False)
# def draw_header(self, context):
# settings = get_scene_settings()
# prefs = get_addon_prefs()
# row = self.layout.row(align=True)
# row.label('VSE Toolbox')
# row.prop(settings, 'project_name', text='')
# project = settings.active_project
# if project and project.type == 'TVSHOW':
# row.prop(project, 'episode_name', text='')
# row.operator('vse_toolbox.load_projects', icon='FILE_REFRESH', text='', emboss=False)
def draw(self, context):
wm = context.window_manager
scn = context.scene
settings = get_scene_settings()
prefs = get_addon_prefs()
row = self.layout.row(align=False)
row.operator('vse_toolbox.load_projects', icon='FILE_REFRESH', text='', emboss=False)
row.prop(settings, 'project_name', text='')
project = settings.active_project
layout = self.layout
col = layout.column()
if project and project.type == 'TVSHOW':
row.prop(project, 'episode_name', text='')
col.prop(settings, 'project_name', text='Project')
# settings = get_scene_settings()
# prefs = get_addon_prefs()
# project = settings.active_project
if project:
if project.type == 'TVSHOW':
col.prop(project, 'episode_name', text='Episodes')
# layout = self.layout
# col = layout.column()
# col.prop(settings, 'project_name', text='Project')
# if project:
# if project.type == 'TVSHOW':
# col.prop(project, 'episode_name', text='Episodes')
#col.separator()
@ -94,7 +120,7 @@ class VSETB_PT_strip(Panel):
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()
@ -113,42 +139,19 @@ class VSETB_PT_sequencer(VSETB_main, Panel):
settings = get_scene_settings()
project = settings.active_project
strip = context.active_sequence_strip
channel = get_channel_name(strip)
col = layout.column()
col.operator('vse_toolbox.set_sequencer', text='Set-Up Sequencer', icon='SEQ_SEQUENCER')
#row = col.row()
shot_label = ''
sequence_label = ''
if project:
episode = project.episode_name
sequence_label = project.sequence_template.format(episode=episode, index=project.sequence_start_number)
shot_label = project.shot_template.format(episode=episode, sequence=sequence_label, index=project.shot_start_number)
#row.separator()
strip = context.active_sequence_strip
channel_name = get_channel_name(strip) or ''
if channel_name == 'Shots':
label = shot_label
elif channel_name == 'Sequences':
label = sequence_label
else:
label = 'Not Supported'
row = col.row(align=True)
if label == 'Not Supported':
row.enabled = False
op = row.operator('vse_toolbox.strips_rename', text=f'Rename {channel_name} ( {label} )', icon='SORTALPHA')
op.channel_name = channel_name
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_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'}
#def draw_header_preset(self, context):
@ -169,7 +172,7 @@ class VSETB_PT_settings(VSETB_main, Panel):
class VSETB_PT_imports(VSETB_main, Panel):
bl_label = "Imports"
bl_parent_id = "VSETB_PT_main"
#bl_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'}
def draw_header_preset(self, context):
@ -197,7 +200,7 @@ class VSETB_PT_presets(PresetPanel, Panel):
class VSETB_PT_exports(VSETB_main, Panel):
bl_label = "Exports"
bl_parent_id = "VSETB_PT_main"
#bl_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'}
def draw_header_preset(self, context):
@ -208,20 +211,20 @@ class VSETB_PT_exports(VSETB_main, Panel):
layout = self.layout
settings = get_scene_settings()
project = settings.active_project
col = layout.column(align=False)
# TODO FAIRE DES VRAIS OPS
layout.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('_', ' ')
layout.operator('vse_toolbox.upload_to_tracker', text=f'Upload to {tracker_label}', icon='EXPORT')
layout.operator('vse_toolbox.export_spreadsheet', text='Export Spreadsheet', icon='SPREADSHEET')
layout.operator('vse_toolbox.export_edl', text='Export edl', icon='SEQ_SEQUENCER')
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_parent_id = "VSETB_PT_main"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
@ -403,8 +406,13 @@ class VSETB_MT_main_menu(Menu):
def draw(self, context):
layout = self.layout
layout.operator('vse_toolbox.open_shot_dir', text='Open Shot', icon='FILE_FOLDER')
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.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")

View File

@ -29,6 +29,7 @@ from vse_toolbox.file_utils import (
)
from vse_toolbox.resources.trackers.kitsu import Kitsu
def load_trackers():
from vse_toolbox.resources.trackers.tracker import Tracker
@ -112,7 +113,9 @@ class VSETB_Prefs(AddonPreferences):
layout = self.layout
layout.prop(self, 'config_path', text='Config Path')
row = layout.row(align=True)
row.prop(self, 'config_path', text='Config Path')
row.operator("vse_toolbox.load_settings", icon="CHECKMARK", text='')
layout.prop(self, "sort_metadata_items", text='Sort Metadata Items')
col = layout.column(align=True)

View File

@ -110,6 +110,12 @@ class MetadataType(PropertyGroup):
class TaskType(PropertyGroup):
color : FloatVectorProperty(subtype='COLOR')
import_enabled : BoolProperty(default=False)
id : StringProperty(default='')
class Sequence(PropertyGroup):
import_enabled : BoolProperty(default=False)
id : StringProperty(default='')
class TaskStatus(PropertyGroup):
@ -211,6 +217,66 @@ class SpreadsheetImport(PropertyGroup):
update_edit: BoolProperty(default=True)
def get_task_status_items(self, context):
settings = get_scene_settings()
project = settings.active_project
status_items = [('CURRENT', 'Current', '')]
if project:
status_items += [(t.name, t.name, '') for t in project.task_statuses]
return status_items
def get_task_type_items(self, context):
settings = get_scene_settings()
project = settings.active_project
if not project:
return [('NONE', 'None', '')]
return [(t.name, t.name, '') for t in project.task_types]
class ImportShots(PropertyGroup):
import_source: EnumProperty(items=[(i, i.title(), '') for i in ('DISK', 'TRACKER')])
import_task: EnumProperty(items=[(i, i.title().replace('_', ' '), '')
for i in ('LAST', 'FROM_LIST', 'ALL')], default='FROM_LIST')
#sequence_dir_template: StringProperty(
# name="Sequence Template", default="$PROJECT_ROOT/sequences/{sequence}")
shot_folder_template: StringProperty(
name="Shot Template", default="$PROJECT_ROOT/sequences/sq{sequence}/sh{shot}")
import_video_template : StringProperty(
name="Video Path", default="./{task}/render/{version}.{ext}")
import_sequence: EnumProperty(items=[(i, i.title().replace('_', ' '), '')
for i in ('SELECTED_STRIPS', 'FROM_LIST', 'ALL')], default='FROM_LIST')
previews_folder: StringProperty(
name="Previews Folder", default="//sources", subtype='DIR_PATH')
class UploadToTracker(PropertyGroup):
render_strips: BoolProperty(default=False)
render_strip_template : StringProperty(
name="Movie Path", default="//render/{project_basename}.{ext}")
task : EnumProperty(items=get_task_type_items)
status : EnumProperty(items=get_task_status_items)
comment : StringProperty()
add_preview : BoolProperty(default=True)
preview_mode : EnumProperty(items=[(m, m.title().replace('_', ' '), '')
for m in ('ONLY_NEW', 'REPLACE', 'ADD')])
set_main_preview : BoolProperty(default=True)
casting : BoolProperty(default=True)
update_frames: BoolProperty(default=True)
custom_data : BoolProperty(default=True)
tasks_comment: BoolProperty(default=True)
def get_episodes_items(self, context):
settings = get_scene_settings()
@ -232,7 +298,10 @@ class Project(PropertyGroup):
id : StringProperty(default='')
shot_start_number : IntProperty(name="Shot Start Number", default=10, min=0)
shot_padding : IntProperty(name="Shot Padding", default=4, min=0, max=10)
sequence_start_number : IntProperty(name="Sequence Start Number", default=10, min=0)
sequence_padding : IntProperty(name="Sequence Padding", default=3, min=0, max=10)
reset_by_sequence : BoolProperty(
name="Reset By Sequence",
@ -247,13 +316,13 @@ class Project(PropertyGroup):
name="Shot Increment", default=10, min=0, step=10)
sequence_template : StringProperty(
name="Sequence Name", default="sq{index:03d}")
name="Sequence Name", default="sq{sequence}")
episode_template : StringProperty(
name="Episode Name", default="ep{index:03d}")
name="Episode Name", default="ep{episode}")
shot_template : StringProperty(
name="Shot Name", default="{sequence}_sh{index:04d}")
name="Shot Name", default="sq{sequence}_sh{shot}")
render_video_template : StringProperty(
name="Movie Path", default="//render/{project_basename}.{ext}")
@ -300,17 +369,15 @@ class Project(PropertyGroup):
task_types : CollectionProperty(type=TaskType)
task_type_index : IntProperty()
task_statuses : CollectionProperty(type=TaskStatus)
sequences : CollectionProperty(type=Sequence)
spreadsheet_import: PointerProperty(type=SpreadsheetImport)
spreadsheet_export: PointerProperty(type=SpreadsheetExport)
type : StringProperty()
import_source: EnumProperty(items=[(i, i.title(), '') for i in ('DISK', 'TRACKER')])
import_task: EnumProperty(items=[(i, i.title(), '') for i in ('LAST', 'SELECTED', 'ALL')], default='LAST')
shot_dir_template: StringProperty(
name="Shot Template", default="$PROJECT_ROOT/sequences/{sequence}/sh{index:04d}")
upload_to_tracker: PointerProperty(type=UploadToTracker)
import_shots: PointerProperty(type=ImportShots)
@property
def active_episode(self):
@ -354,8 +421,6 @@ class Project(PropertyGroup):
return cell_types
def set_spreadsheet(self):
cell_names = ['Sequence', 'Shot', 'Nb Frames', 'Description']
if self.type == 'TVSHOW':
cell_names.insert(0, 'Episode')
@ -563,6 +628,7 @@ def on_project_updated(self, context):
os.environ['TRACKER_PROJECT_ID'] = settings.active_project.id
class VSETB_PGT_scene_settings(PropertyGroup):
projects : CollectionProperty(type=Project)
@ -649,17 +715,21 @@ class VSETB_PGT_strip_settings(PropertyGroup):
channel = get_channel_name(strip)
if channel == 'Sequences':
data = parse(strip.name, template=project.sequence_template)
data['index'] = int(data['index'])
data['sequence'] = strip.name
data = parse(project.sequence_template, strip.name)
#data['index'] = int(data['index'])
#data['sequence'] = strip.name
data['strip'] = strip.name
#data['shot'] = project.shot_template
elif channel == "Shots":
data = {}
if sequence_strip_name := get_strip_sequence_name(strip):
data = parse(project.sequence_template, sequence_strip_name)
data['sequence_strip'] = sequence_strip_name
data.update(parse(project.shot_template, strip.name))
#data['index'] = int(data['index'])
data = parse(strip.name, template=project.shot_template)
data['index'] = int(data['index'])
data['sequence'] = get_strip_sequence_name(strip)
data['strip'] = strip.name
#data['shot'] = project.shot_template
else:
@ -677,11 +747,14 @@ classes = (
Episode,
Metadata,
MetadataType,
Sequence,
TaskType,
ShotTasks,
ShotTask,
SpreadsheetImport,
SpreadsheetExport,
ImportShots,
UploadToTracker,
Project,
VSETB_UL_spreadsheet_import,
VSETB_UL_spreadsheet_export,