Blender Kitsu: Add Operator to Import Playblasts into Edit #274

Merged
Nick Alberelli merged 16 commits from TinyNick/blender-studio-pipeline:feature/import-playblasts into main 2024-04-03 17:38:07 +02:00
10 changed files with 281 additions and 67 deletions

Binary file not shown.

BIN
docs/media/addons/blender_kitsu/import_playblast.jpg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/media/addons/blender_kitsu/media_panel.jpg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/media/addons/blender_kitsu/shot_image_sequence.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -16,6 +16,7 @@ For each new task type, Anim/Layout etc needs to be added manually, then it can
Returning to your edit .blend file, we can now load the playblast from the animation file into the edit. Returning to your edit .blend file, we can now load the playblast from the animation file into the edit.
1. Open your edit .blend file inside the directory `/your_project_name/svn/edit` 1. Open your edit .blend file inside the directory `/your_project_name/svn/edit`
2. From the Sequencer Header select `Add>Movie` 2. Select the Metadata Strip associated with your shot
3. Navigate to the directory of the playblast for your shot's .blend file `your_project_name/shared/footage/pro/{sequence_name}/{shot_name}/{file_name}` and select the `.mp4` file 3. From the Sequencer Side Panel select `Import 1 Shot Playblast`
4. Place the new shot at the same timing as the corresponding metastrip 3. Select the Task Type you would like to load the playblast from and an empty channel
4. Your new playblast will be imported with the same timing as the corresponding metadata strip

View File

@ -16,4 +16,4 @@ Renders approved by the render review Add-On can be automatically imported into
1. Open your Edit .blend file 1. Open your Edit .blend file
2. Select the video strip representing the shot that has an approved render. In the Kitsu Sidebar under General Tools select, `^` to load the next playblast from that shot automatically, which is an **mp4 preview** of your final render 2. Select the video strip representing the shot that has an approved render. In the Kitsu Sidebar under General Tools select, `^` to load the next playblast from that shot automatically, which is an **mp4 preview** of your final render
3. Select the video strips representing all the shots you have approved renders for. Use `Shot as Image Sequence` to import the final image sequences for each shot as EXR or JPG and load it to a new channel in the VSE 3. Select the metadata strips representing all the shots you have approved renders for. Use `Import Image Sequence` operator to import the final image sequences for each shot as EXR or JPG and load it to a new channel in the VSE

View File

@ -14,7 +14,7 @@ blender-kitsu is a Blender Add-on to interact with Kitsu from within Blender. It
- [Push](#push) - [Push](#push)
- [Pull](#pull) - [Pull](#pull)
- [Multi Edit](#multi-edit) - [Multi Edit](#multi-edit)
- [Shot as Image Sequence](#shot-as-image-sequence) - [Import Media](#import-media)
- [General Sequence Editor Tools](#general-sequence-editor-tools) - [General Sequence Editor Tools](#general-sequence-editor-tools)
- [Context](#context) - [Context](#context)
- [Animation Tools](#animation-tools) - [Animation Tools](#animation-tools)
@ -192,13 +192,21 @@ The `Multi Edit` panel only appears when you select multiple metadata strips tha
![image info](/media/addons/blender_kitsu/sqe_multi_edit.jpg) ![image info](/media/addons/blender_kitsu/sqe_multi_edit.jpg)
It is meant to be way to quickly setup lots of shots if they don't exist on Kitsu yet. You specify the sequence all shots should belong to and adjust the `Shot Counter Start` value. In the preview property you can see how all shots will be named when you execute the `Multi Edit Strip` operator. <br/> It is meant to be way to quickly setup lots of shots if they don't exist on Kitsu yet. You specify the sequence all shots should belong to and adjust the `Shot Counter Start` value. In the preview property you can see how all shots will be named when you execute the `Multi Edit Strip` operator. <br/>
##### Import Media
A collection of operators to Import media based on the Shot associated with the selected metadata strip(s). <br/>
##### Shot as Image Sequence ![Media Panel](/media/addons/blender_kitsu/media_panel.jpg)
The `Shot as Image Sequence` Operator will replace a playblast from your Playblast Root directory with an image sequence located in the Frames Root directory. The Shots Directory and the Frames Directory should have matching folder structures. Typically the format is `/{sequence_name}/{shot_name}/{shot_name}-{shot_task}/` ##### Import Playblast
With a metadata strip selected `Import Playblast` Operator will find an image sequence of a given task type located in the Frames Root directory.
![Import Playblast](/media/addons/blender_kitsu/import_playblast.jpg)
![Shot as Image Sequence](/media/addons/blender_kitsu/Shot_as_Image_Sequence.jpg) Use this operator to import image sequences that have been approved via the [Render Review Add-On](/addons/render_review) Image Sequences can be loaded as either `EXR` or `JPG` sequences.
Use this operator to replace playblasts with image sequences that have been approved via the [Render Review Add-On](/addons/render_review) Image Sequences can be loaded as either `EXR` or `JPG` sequences. ##### Import Image Sequence
With a metadata strip selected `Import Image Sequence` Operator will find an image sequence of a given task type located in the Frames Root directory.
![Import Image Sequence](/media/addons/blender_kitsu/shot_image_sequence.jpg)
Use this operator to import image sequences that have been approved via the [Render Review Add-On](/addons/render_review) Image Sequences can be loaded as either `EXR` or `JPG` sequences.
###### Advanced Settings ###### Advanced Settings
If you check the `Advanced` checkbox next to the counter value, you have access to advance settings to customize the operator even more. If you check the `Advanced` checkbox next to the counter value, you have access to advance settings to customize the operator even more.

View File

@ -20,6 +20,8 @@
import os import os
import re import re
from bpy.types import Context
import gazu import gazu
import contextlib import contextlib
import colorsys import colorsys
@ -2195,13 +2197,7 @@ class KITSU_OT_sqe_scan_for_media_updates(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class KITSU_OT_shot_image_sequence(bpy.types.Operator): def get_used_channels(self: Any, context: bpy.types.Context, edit_text: str = "") -> List[str]:
bl_idname = "kitsu.shot_image_sequence"
bl_label = "Shot as Image Sequence"
bl_description = "Import image sequences for selected clips"
bl_options = {"REGISTER", "UNDO"}
def get_used_channels(self: Any, context: bpy.types.Context, edit_text: str) -> List[str]:
used_channels = [] used_channels = []
for seq in context.scene.sequence_editor.sequences_all: for seq in context.scene.sequence_editor.sequences_all:
used_channels.append(seq.channel) used_channels.append(seq.channel)
@ -2213,6 +2209,122 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
return [f"{channel}" for channel in aval_channels] return [f"{channel}" for channel in aval_channels]
def get_shot_task_types_enum_list(self, context: bpy.types.Context) -> List[Tuple[str, str, str]]:
active_project = cache.project_active_get()
return [
(t.id, t.name, "")
for t in TaskType.all_shot_task_types()
if t.id in active_project.task_types
]
class KITSU_OT_sqe_import_playblast(bpy.types.Operator):
bl_idname = "kitsu.sqe_import_playblast"
bl_label = "Import Playblast"
bl_description = "Import playblast for selected metadata strips"
bl_options = {"REGISTER", "UNDO"}
channel_selection: bpy.props.StringProperty( # type: ignore
name="Channel",
description="Choose an empty target channel to place playblasts onto",
search=get_used_channels,
search_options={'SORT'},
)
task_type: bpy.props.EnumProperty( # type: ignore
name="Task Type",
description="Choose a task type to import playblasts for",
items=get_shot_task_types_enum_list,
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
if not sqe:
return False
if not cache.project_active_get():
cls.poll_message_set("No Kitsu Project Found check Add-on Preferences")
return False
for strip in context.selected_sequences:
if strip.kitsu.shot_id == "":
cls.poll_message_set(f"Selected strip {strip.name} is not metadata strip'")
return False
if len(bpy.context.selected_sequences) == 0:
cls.poll_message_set("Please select one or more metadata strips")
return False
return True
def invoke(self, context, event):
channels = [int(x) for x in get_used_channels(self, context)]
strip = context.selected_sequences[0]
channel = min(channels, key=lambda x: abs(x - strip.channel))
self.channel_selection = f"{channel}"
return context.window_manager.invoke_props_dialog(self, width=500)
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
layout.prop(self, "channel_selection")
layout.prop(self, "task_type")
def execute(self, context: Context) -> Set[str] | Set[int]:
succeeded: Set[str] = set()
failed: Set[str] = set()
sequences = context.scene.sequence_editor.sequences
metadata_strips = [
strip for strip in context.selected_sequences if strip.kitsu.shot_id != ''
]
for metadata_strip in metadata_strips:
# TODO add try except if ID is not valid, do same for shot as image sequence.
shot = Shot.by_id(metadata_strip.kitsu.shot_id)
task_type_short_name = TaskType.by_id(self.task_type).get_short_name()
filepath = shot.get_latest_playblast_file(context, task_type_short_name)
if filepath:
playblast = sequences.new_movie(
name=Path(filepath).name,
filepath=filepath,
frame_start=int(metadata_strip.frame_start),
channel=int(self.channel_selection),
)
if playblast.frame_final_end > metadata_strip.frame_final_end:
playblast.frame_final_end = metadata_strip.frame_final_end
succeeded.add(metadata_strip.name)
else:
failed.add(metadata_strip.name)
if len(metadata_strips) == 1:
if len(failed) == 1:
self.report({"WARNING"}, f"Failed to import Playblast `{failed[0]}` does not exist")
return {"CANCELLED"}
if len(succeeded) == 1:
self.report({"INFO"}, f"Imported Playblast from `{succeeded[0]}`")
return {"FINISHED"}
report_str = f"Imported {len(succeeded)} Playblast"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
self.report(
{report_state},
report_str,
)
return {'FINISHED'}
class KITSU_OT_sqe_import_image_sequence(bpy.types.Operator):
bl_idname = "kitsu.sqe_import_image_sequence"
bl_label = "Import Image Sequence"
bl_description = "Import Image Sequence for selected metadata strips"
bl_options = {"REGISTER", "UNDO"}
channel_selection: bpy.props.StringProperty( channel_selection: bpy.props.StringProperty(
name="Channel", name="Channel",
description="Choose an empty target channel to place image sequences onto", description="Choose an empty target channel to place image sequences onto",
@ -2234,6 +2346,12 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
default=True, default=True,
) )
task_type: bpy.props.EnumProperty( # type: ignore
name="Task Type",
description="Choose a task type to import playblasts for",
items=get_shot_task_types_enum_list,
)
@classmethod @classmethod
def poll(cls, context: bpy.types.Context) -> bool: def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor sqe = context.scene.sequence_editor
@ -2242,9 +2360,9 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
if not cache.project_active_get(): if not cache.project_active_get():
cls.poll_message_set("No Kitsu Project Found check Add-on Preferences") cls.poll_message_set("No Kitsu Project Found check Add-on Preferences")
return False return False
for sqe in context.selected_sequences: for strip in context.selected_sequences:
if sqe.type != 'MOVIE': if strip.kitsu.shot_id == "":
cls.poll_message_set("Selected strips must be 'MOVIE'") cls.poll_message_set(f"Selected strip {strip.name} is not metadata strip'")
return False return False
if len(bpy.context.selected_sequences) == 0: if len(bpy.context.selected_sequences) == 0:
cls.poll_message_set("Please select a 'MOVIE' strip") cls.poll_message_set("Please select a 'MOVIE' strip")
@ -2268,7 +2386,7 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
scene.view_settings.view_transform = 'Filmic' scene.view_settings.view_transform = 'Filmic'
def invoke(self, context, event): def invoke(self, context, event):
channels = [int(x) for x in self.get_used_channels(context, "")] channels = [int(x) for x in get_used_channels(self, context)]
strip = context.selected_sequences[0] strip = context.selected_sequences[0]
channel = min(channels, key=lambda x: abs(x - strip.channel)) channel = min(channels, key=lambda x: abs(x - strip.channel))
self.channel_selection = f"{channel}" self.channel_selection = f"{channel}"
@ -2276,6 +2394,7 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
def draw(self, context: bpy.types.Context) -> None: def draw(self, context: bpy.types.Context) -> None:
layout = self.layout layout = self.layout
layout.prop(self, "task_type")
layout.prop(self, "channel_selection") layout.prop(self, "channel_selection")
layout.prop(self, "file_type") layout.prop(self, "file_type")
layout.prop(self, "set_color_space") layout.prop(self, "set_color_space")
@ -2288,23 +2407,13 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
return match.group(1) return match.group(1)
return return
def get_metadata_strip(self, context, strip): def import_strip(self, context, metadata_strip, directory, channel):
name = self.get_shot_name(strip)
for strip in context.scene.sequence_editor.sequences_all:
if strip.name == name:
return strip
return
def import_strip(self, context, strip, directory, channel):
# https://blender.stackexchange.com/questions/286946/how-to-add-image-sequence-in-sequencer-via-python # https://blender.stackexchange.com/questions/286946/how-to-add-image-sequence-in-sequencer-via-python
frame_start = strip.frame_final_start frame_start = metadata_strip.frame_final_start
frame_end = strip.frame_final_end frame_end = metadata_strip.frame_final_end
files = [] files = []
metadata_strip = self.get_metadata_strip(context, strip)
if not metadata_strip:
self.report({'ERROR'}, f"No Metadata Strip found for {strip.name}")
return {'CANCELLED'}
shot = Shot.by_id(metadata_strip.kitsu.shot_id) shot = Shot.by_id(metadata_strip.kitsu.shot_id)
start_frame = ( start_frame = (
shot.data.get('3d_start') if shot.data.get('3d_start') else bkglobals.FRAME_START shot.data.get('3d_start') if shot.data.get('3d_start') else bkglobals.FRAME_START
@ -2313,7 +2422,7 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
if file.name.endswith("mp4"): if file.name.endswith("mp4"):
continue continue
frame_number = int(file.name.split(".")[0]) frame_number = int(file.name.split(".")[0])
duration = strip.frame_duration + start_frame duration = metadata_strip.frame_duration + start_frame
if self.file_type in file.name and frame_number < duration: if self.file_type in file.name and frame_number < duration:
files.append({"name": file.name}) files.append({"name": file.name})
@ -2345,24 +2454,24 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
fit_method='FIT', fit_method='FIT',
) )
if len_strip + 1 != len(context.scene.sequence_editor.sequences_all): if len_strip + 1 != len(context.scene.sequence_editor.sequences_all):
print(f"Failed to import image sequence for {strip.name}") print(f"Failed to import image sequence for {metadata_strip.name}")
return return
new_strip = context.selected_sequences[0] new_strip = context.selected_sequences[0]
new_strip.animation_offset_end = strip.animation_offset_end new_strip.animation_offset_end = metadata_strip.animation_offset_end
new_strip.animation_offset_start = strip.animation_offset_start new_strip.animation_offset_start = metadata_strip.animation_offset_start
new_strip.frame_offset_end = strip.frame_offset_end new_strip.frame_offset_end = metadata_strip.frame_offset_end
new_strip.frame_offset_start = strip.frame_offset_start new_strip.frame_offset_start = metadata_strip.frame_offset_start
new_strip.frame_start = strip.frame_start new_strip.frame_start = metadata_strip.frame_start
new_strip.channel = channel new_strip.channel = channel
new_strip.name = f"{self.get_shot_name(strip)}{self.file_type.lower()}" new_strip.name = f"{self.get_shot_name(metadata_strip)}{self.file_type.lower()}"
new_strip.colorspace_settings.name = new_strip.colorspace_settings.name new_strip.colorspace_settings.name = new_strip.colorspace_settings.name
def get_shot_seq_directory(self, context, strip): def get_shot_seq_directory(self, context, filepath):
addon_prefs = prefs.addon_prefs_get(context) addon_prefs = prefs.addon_prefs_get(context)
path_string = os.path.realpath(bpy.path.abspath(strip.filepath)) path_string = os.path.realpath(bpy.path.abspath(filepath))
path = Path( path = Path(
path_string.replace( path_string.replace(
addon_prefs.shot_playblast_root_dir, addon_prefs.frames_root_dir addon_prefs.shot_playblast_root_dir, addon_prefs.frames_root_dir
@ -2372,6 +2481,8 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
def execute(self, context: bpy.types.Context) -> Set[str]: def execute(self, context: bpy.types.Context) -> Set[str]:
# Get closest empty channel # Get closest empty channel
succeeded = []
failed = []
channel = int(self.channel_selection) channel = int(self.channel_selection)
addon_prefs = prefs.addon_prefs_get(context) addon_prefs = prefs.addon_prefs_get(context)
if not ( if not (
@ -2386,12 +2497,42 @@ class KITSU_OT_shot_image_sequence(bpy.types.Operator):
if self.set_color_space: if self.set_color_space:
self.set_scene_colorspace(context) self.set_scene_colorspace(context)
for strip in [strip for strip in context.selected_sequences if strip.type == 'MOVIE']: metadata_strips = [
directory = self.get_shot_seq_directory(context, strip) strip for strip in context.selected_sequences if strip.kitsu.shot_id != ''
]
for strip in metadata_strips:
shot = Shot().by_id(strip.kitsu.shot_id)
# TODO pass task type as variable
task_type_short_name = TaskType.by_id(self.task_type).get_short_name()
filepath = shot.get_latest_playblast_file(context, task_type_short_name)
directory = self.get_shot_seq_directory(context, filepath)
if not directory.exists(): if not directory.exists():
self.report({"ERROR"}, f"{directory._str} does not exist") failed.append(str(directory))
return {"CANCELLED"} continue
self.import_strip(context, strip, directory, channel) self.import_strip(context, strip, directory, channel)
succeeded.append(str(directory))
if len(metadata_strips) == 1:
if len(failed) == 1:
self.report(
{"WARNING"}, f"Failed to import Image Sequence `{failed[0]}` does not exist"
)
return {"CANCELLED"}
if len(succeeded) == 1:
self.report({"INFO"}, f"Imported Image Sequence from `{succeeded[0]}`")
return {"FINISHED"}
report_str = f"Imported {len(succeeded)} Image Sequences"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
return {"FINISHED"} return {"FINISHED"}
@ -2594,7 +2735,8 @@ classes = [
KITSU_OT_sqe_scan_for_media_updates, KITSU_OT_sqe_scan_for_media_updates,
KITSU_OT_sqe_change_strip_source, KITSU_OT_sqe_change_strip_source,
KITSU_OT_sqe_clear_update_indicators, KITSU_OT_sqe_clear_update_indicators,
KITSU_OT_shot_image_sequence, KITSU_OT_sqe_import_image_sequence,
KITSU_OT_sqe_import_playblast,
] ]

View File

@ -50,7 +50,8 @@ from ..sqe.ops import (
KITSU_OT_sqe_scan_for_media_updates, KITSU_OT_sqe_scan_for_media_updates,
KITSU_OT_sqe_change_strip_source, KITSU_OT_sqe_change_strip_source,
KITSU_OT_sqe_clear_update_indicators, KITSU_OT_sqe_clear_update_indicators,
KITSU_OT_shot_image_sequence, KITSU_OT_sqe_import_image_sequence,
KITSU_OT_sqe_import_playblast,
) )
from pathlib import Path from pathlib import Path
@ -121,6 +122,7 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
if self.poll_pull(context): if self.poll_pull(context):
self.draw_pull(context) self.draw_pull(context)
self.draw_media(context)
if self.poll_debug(context): if self.poll_debug(context):
self.draw_debug(context) self.draw_debug(context)
@ -639,6 +641,30 @@ class KITSU_PT_sqe_shot_tools(bpy.types.Panel):
icon="MODIFIER_ON", icon="MODIFIER_ON",
) )
def draw_media(self, context: bpy.types.Context) -> None:
sel_metadata_strips = [strip for strip in context.selected_sequences if strip.kitsu.linked]
noun = get_selshots_noun(len(sel_metadata_strips), prefix=f"{len(sel_metadata_strips)}")
playblast = "Playblast" if len(sel_metadata_strips) <= 1 else "Playblasts"
sequence = "Sequence" if len(sel_metadata_strips) <= 1 else "Sequences"
# Create box.
layout = self.layout
box = layout.box()
box.label(text="Media", icon="RENDER_ANIMATION")
box.operator(
KITSU_OT_sqe_import_playblast.bl_idname,
text=f"Import {noun} {playblast}",
icon="FILE_MOVIE",
)
box.operator(
KITSU_OT_sqe_import_image_sequence.bl_idname,
text=f"Import {noun} Image {sequence}",
icon="RENDER_RESULT",
)
class KITSU_PT_sqe_general_tools(bpy.types.Panel): class KITSU_PT_sqe_general_tools(bpy.types.Panel):
""" """
@ -684,7 +710,6 @@ class KITSU_PT_sqe_general_tools(bpy.types.Panel):
box = layout.box() box = layout.box()
box.label(text="General", icon="MODIFIER") box.label(text="General", icon="MODIFIER")
box.operator(KITSU_OT_shot_image_sequence.bl_idname)
# Scan for outdated media and reset operator. # Scan for outdated media and reset operator.
row = box.row(align=True) row = box.row(align=True)
row.operator( row.operator(

View File

@ -27,6 +27,8 @@ import gazu
from .logger import LoggerFactory from .logger import LoggerFactory
from . import bkglobals from . import bkglobals
from . import prefs from . import prefs
from .models import FileListModel
import mimetypes
logger = LoggerFactory.getLogger() logger = LoggerFactory.getLogger()
@ -614,24 +616,54 @@ class Shot(Entity):
def get_output_collection_name(self, task_type_short_name: str) -> str: def get_output_collection_name(self, task_type_short_name: str) -> str:
return f"{self.get_task_name(task_type_short_name)}{bkglobals.DELIMITER}output" return f"{self.get_task_name(task_type_short_name)}{bkglobals.DELIMITER}output"
def get_dir(self, context) -> str: def get_shot_folder_tree(self, base_path: Path) -> str:
project_root_dir = prefs.project_root_dir_get(context)
all_shots_dir = project_root_dir.joinpath('pro').joinpath('shots')
# Add Episode to Path if available # Add Episode to Path if available
if self.episode_id: if self.episode_id:
base_dir = all_shots_dir.joinpath(self.episode_name) base_dir = base_path.joinpath(self.episode_name)
else: else:
base_dir = all_shots_dir base_dir = base_path
seq = self.get_sequence() seq = self.get_sequence()
shot_dir = base_dir.joinpath(seq.name).joinpath(self.name) shot_dir = base_dir.joinpath(seq.name).joinpath(self.name)
return shot_dir.__str__() return shot_dir
def get_dir(self, context) -> str:
project_root_dir = prefs.project_root_dir_get(context)
all_shots_dir = project_root_dir.joinpath('pro').joinpath('shots')
return str(self.get_shot_folder_tree(all_shots_dir))
def get_filepath(self, context, task_type_short_name: str) -> str: def get_filepath(self, context, task_type_short_name: str) -> str:
file_name = self.get_task_name(task_type_short_name) + '.blend' file_name = self.get_task_name(task_type_short_name) + '.blend'
return Path(self.get_dir(context)).joinpath(file_name).__str__() return Path(self.get_dir(context)).joinpath(file_name).__str__()
def get_playblast_dir(self, context, task_type_short_name: str) -> str:
addon_prefs = prefs.addon_prefs_get(context)
playblsat_dir = addon_prefs.shot_playblast_root_dir
shot_dir = self.get_shot_folder_tree(Path(playblsat_dir))
task_dir = shot_dir.joinpath(self.name + bkglobals.DELIMITER + task_type_short_name)
return task_dir.__str__()
def get_latest_playblast_file(self, context, task_type_short_name: str):
filemodel = FileListModel()
filemodel.reset()
playblast_dir = Path(self.get_playblast_dir(context, task_type_short_name))
filemodel.root_path = playblast_dir
if len(filemodel.items) < 1:
return
playblast_files = set()
for file in filemodel.items:
filepath = playblast_dir.joinpath(file)
if mimetypes.guess_type(filepath)[0].startswith('video'):
playblast_files.add(filepath)
playblast_files = sorted(playblast_files, key=lambda x: str(x), reverse=True)
file = playblast_files[0]
if not file.exists():
return
return str(file)
def update_data(self, data: Dict[str, Any]) -> Shot: def update_data(self, data: Dict[str, Any]) -> Shot:
gazu.shot.update_shot_data(asdict(self), data=data) gazu.shot.update_shot_data(asdict(self), data=data)
if not self.data: if not self.data: