Files
blender-portable-repo/scripts/addons/blender_kitsu/sqe/ops.py
T
2026-03-17 14:58:51 -06:00

2720 lines
94 KiB
Python

# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import re
from bpy.types import Context
import gazu
import contextlib
import colorsys
import random
from pathlib import Path
from typing import Dict, List, Set, Optional, Tuple, Any
import datetime
import bpy
from .. import cache, util, prefs, bkglobals
from ..sqe import push, pull, checkstrip, opsdata, checksqe
from ..logger import LoggerFactory
from ..types import (
Cache,
Sequence,
Shot,
TaskType,
TaskStatus,
Task,
)
logger = LoggerFactory.getLogger()
class KITSU_OT_sqe_push_shot_meta(bpy.types.Operator):
bl_idname = "kitsu.sqe_push_shot_meta"
bl_label = "Push Shot Metadata"
bl_options = {"INTERNAL"}
bl_description = (
"Pushes metadata of all selected sequences to server. "
"This includes frame information, name, project, sequence and description"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(prefs.session_auth(context))
def execute(self, context: bpy.types.Context) -> Set[str]:
succeeded = []
failed = []
logger.info("-START- Pushing Metadata")
# Get strips.
selected_sequences = context.selected_sequences
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
# Sort strips.
selected_sequences = sorted(selected_sequences, key=lambda strip: strip.frame_final_start)
# Begin progress update.
context.window_manager.progress_begin(0, len(selected_sequences))
# Clear cache.
Cache.clear_all()
# Track sequence ids that were processed to later update sequence.data["color"] on kitu.
sequence_ids: List[str] = []
# Shots.
for idx, strip in enumerate(selected_sequences):
context.window_manager.progress_update(idx)
if not checkstrip.is_valid_type(strip):
# Failed.append(strip).
continue
# Only if strip is linked to sevrer.
if not checkstrip.is_linked(strip):
# Failed.append(strip).
continue
# Check if shot is still available by id.
shot = checkstrip.shot_exists_by_id(strip, clear_cache=False)
if not shot:
failed.append(strip)
continue
# Push update to shot.
push.shot_meta(strip, shot)
# Append sequence id.
if shot.parent_id not in sequence_ids:
sequence_ids.append(shot.parent_id)
succeeded.append(strip)
# End progress update.
context.window_manager.progress_update(len(selected_sequences))
context.window_manager.progress_end()
# Sequences.
# Begin second progress update for sequences.
context.window_manager.progress_begin(0, len(sequence_ids))
for idx, seq_id in enumerate(sequence_ids):
context.window_manager.progress_update(idx)
sequence = Sequence.by_id(seq_id)
opsdata.push_sequence_color(context, sequence)
# End second progress update.
context.window_manager.progress_update(len(sequence_ids))
context.window_manager.progress_end()
# Report.
report_str = f"Pushed Metadata of {len(succeeded)} Shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Pushing Metadata")
return {"FINISHED"}
class KITSU_OT_sqe_push_new_shot(bpy.types.Operator):
bl_idname = "kitsu.sqe_push_new_shot"
bl_label = "Submit New Shot"
bl_description = "Creates a new shot for each selected sequence strip on server. Checks if shot already exists"
confirm: bpy.props.BoolProperty(name="confirm")
add_tasks: bpy.props.BoolProperty(name="Add Default Tasks")
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
# Needs to be logged in, active project.
sequences = context.selected_sequences
if not sequences:
return False
nr_of_shots = len(sequences)
if nr_of_shots == 1:
strip = context.scene.sequence_editor.active_strip
return bool(
prefs.session_auth(context)
and cache.project_active_get()
and strip.kitsu.sequence_name
and (strip.kitsu.manual_shot_name or strip.kitsu.shot_name)
)
return bool(prefs.session_auth(context) and cache.project_active_get())
def execute(self, context: bpy.types.Context) -> Set[str]:
if not self.confirm:
self.report({"WARNING"}, "Submit new shots aborted")
return {"CANCELLED"}
project_active = cache.project_active_get()
succeeded = []
failed = []
no_sequence_id = []
logger.info("-START- Submitting new shots to: %s", project_active.name)
# Clear cache.
Cache.clear_all()
# Get strips.
selected_sequences = context.selected_sequences
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
# Sort strips.
selected_sequences = sorted(selected_sequences, key=lambda strip: strip.frame_final_start)
# Begin progress update.
context.window_manager.progress_begin(0, len(selected_sequences))
for idx, strip in enumerate(selected_sequences):
context.window_manager.progress_update(idx)
if not checkstrip.is_valid_type(strip):
# Failed.append(strip).
continue
# Check if user initialized shot.
if not checkstrip.is_initialized(strip):
# Failed.append(strip).
continue
# Check if strip is already linked to server.
if checkstrip.is_linked(strip):
failed.append(strip)
continue
# Check if user provided enough info.
if not checkstrip.has_meta(strip):
failed.append(strip)
continue
if strip.kitsu.sequence_id == "":
no_sequence_id.append(strip)
continue
# Check if seq already to server > create it.
seq = checkstrip.seq_exists_by_id(strip, project_active, clear_cache=False)
if not seq:
seq = push.new_sequence(strip, project_active)
# Check if shot already to server > create it.
shot = checkstrip.shot_exists_by_name(strip, project_active, seq, clear_cache=False)
if shot:
failed.append(strip)
continue
if strip.kitsu.manual_shot_name:
strip.kitsu.shot_name = strip.kitsu.manual_shot_name
# Push update to sequence.
opsdata.push_sequence_color(context, seq)
# Push update to shot.
shot = push.new_shot(strip, seq, project_active, add_tasks=self.add_tasks)
pull.shot_meta(strip, shot)
succeeded.append(strip)
# Rename strip.
strip.name = shot.name
# End progress update.
context.window_manager.progress_update(len(selected_sequences))
context.window_manager.progress_end()
# Clear cache.
Cache.clear_all()
# Report.
report_str = f"Submitted {len(succeeded)} new shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
if no_sequence_id:
report_state = "WARNING"
report_str += f" | Sequence metadata was not set for: {len(failed)} strips"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Submitting new shots to: %s", project_active.name)
util.ui_redraw()
return {"FINISHED"}
def invoke(self, context, event):
self.confirm = False
return context.window_manager.invoke_props_dialog(self, width=500)
def draw(self, context):
project_active = cache.project_active_get()
selected_sequences = context.selected_sequences
layout = self.layout
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
strips_to_submit = [
s
for s in selected_sequences
if s.kitsu.initialized
and not s.kitsu.linked
and (s.kitsu.manual_shot_name or s.kitsu.sequence_name)
]
if len(selected_sequences) > 1:
noun = "%i Shots" % len(strips_to_submit)
else:
noun = "this Shot"
# Production.
row = layout.row()
row.label(text=f"Production: {project_active.name}", icon="FILEBROWSER")
# Confirm dialog.
col = layout.column()
col.prop(
self,
"confirm",
text="Submit %s to server. Will skip shots if they already exist" % (noun.lower()),
)
col.prop(self, "add_tasks", text="Add Tasks with the default status to shot(s)")
class KITSU_OT_sqe_push_new_sequence(bpy.types.Operator):
bl_idname = "kitsu.sqe_push_new_sequence"
bl_label = "Submit New Sequence"
bl_description = "Creates new sequence on server. Will skip if sequence already exists"
sequence_name: bpy.props.StringProperty(
name="Name", default="", description="Name of new sequence"
)
confirm: bpy.props.BoolProperty(name="confirm", default=True)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
# Needs to be logged in, active project.
return bool(prefs.session_auth(context) and cache.project_active_get())
def execute(self, context: bpy.types.Context) -> Set[str]:
if not self.confirm:
self.report({"WARNING"}, "Submit new sequence aborted")
return {"CANCELLED"}
if not self.sequence_name:
self.report({"WARNING"}, "Invalid sequence name")
return {"CANCELLED"}
project_active = cache.project_active_get()
episode_active = cache.episode_active_get()
sequence = project_active.get_sequence_by_name(self.sequence_name, episode_active)
if sequence:
self.report(
{"WARNING"},
f"Sequence: {sequence.name} already exists on server",
)
return {"CANCELLED"}
# Create sequence.
sequence = project_active.create_sequence(
self.sequence_name,
episode_active.id if episode_active else None,
)
# Push sequence color.
opsdata.push_sequence_color(context, sequence)
# Clear cache.
Cache.clear_all()
cache.reset_sequences_enum_list()
self.report(
{"INFO"},
f"Submitted new sequence: {sequence.name}",
)
# Avoids circular import
from .ui import KITSU_PT_sqe_shot_tools as seq_tools
# set newly created sequence as target for multi edit or active strip
if seq_tools.poll_multi_edit(context):
context.window_manager.selected_sequence_name = sequence.name
else:
context.active_sequence_strip.kitsu.sequence_name = sequence.name
logger.info("Submitted new sequence: %s", sequence.name)
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=300)
def draw(self, context):
layout = self.layout
project_active = cache.project_active_get()
# Production.
row = layout.row()
row.label(text=f"Production: {project_active.name}", icon="FILEBROWSER")
# Sequence name.
row = layout.row()
row.prop(self, "sequence_name")
# Confirm dialog.
col = layout.column()
col.prop(
self,
"confirm",
text="Submit sequence to server. Will skip if already exists",
)
class KITSU_OT_sqe_init_strip(bpy.types.Operator):
"""
Operator that initializes a regular sequence strip to a 'kitsu' shot.
Only sets strip.kitsu.initialized = True. But this is required for further
operations and to differentiate between regular sequence strip and kitsu shot strip.
"""
bl_idname = "kitsu.sqe_init_strip"
bl_label = "Initialize Shot"
bl_description = "Initializes selected sequence strip as kitsu strip"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
succeeded = []
failed = []
logger.info("-START- Initializing shots")
# Get strips.
selected_sequences = context.selected_sequences
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
# Sort strips.
selected_sequences = sorted(selected_sequences, key=lambda strip: strip.frame_final_start)
for strip in selected_sequences:
if not checkstrip.is_valid_type(strip):
continue
if strip.kitsu.initialized:
logger.info("%s already initialized", strip.name)
continue
strip.kitsu.initialized = True
# Apply strip.kitsu.frame_start_offset.
opsdata.init_start_frame_offset(strip)
succeeded.append(strip)
logger.info("Initiated strip: %s as shot", strip.name)
# Report.
report_str = f"Initiated {len(succeeded)} shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Initializing shots")
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_link_sequence(bpy.types.Operator):
"""
Gets all sequences that are available in server for active production and let's user select. Invokes a search popup on click.
"""
bl_idname = "kitsu.sqe_link_sequence"
bl_label = "Link Sequence"
bl_options = {"REGISTER", "UNDO"}
bl_property = "enum_prop"
bl_description = "Links selected sequence strip to an existing sequence on server"
enum_prop: bpy.props.EnumProperty(
items=cache.get_sequences_enum_list,
) # type: ignore
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
if not sqe:
return False
strip = sqe.active_strip
return bool(
prefs.session_auth(context)
and cache.project_active_get()
and strip
and context.selected_sequences
and checkstrip.is_valid_type(strip)
)
def execute(self, context: bpy.types.Context) -> Set[str]:
strip = context.scene.sequence_editor.active_strip
sequence_id = self.enum_prop
if not sequence_id:
return {"CANCELLED"}
# Set sequence properties.
seq = Sequence.by_id(sequence_id)
strip.kitsu.sequence_name = seq.name
strip.kitsu.sequence_id = seq.id
# Pull sequence color.
opsdata.append_sequence_color(context, seq)
util.ui_redraw()
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class KITSU_OT_sqe_link_shot(bpy.types.Operator):
"""
Operator that invokes ui which shows user all available shots on server.
It is used to 'link' a sequence strip to an already existent shot on server.
Pulls all metadata after selecting shot.
"""
bl_idname = "kitsu.sqe_link_shot"
bl_label = "Link Shot"
bl_description = (
"Links selected sequence strip to shot on server. Pulls all metadata of shot from server"
)
bl_options = {"REGISTER", "UNDO"}
use_url: bpy.props.BoolProperty(
name="Use URL",
description="Use URL of shot on server to initiate strip. Paste complete URL",
)
url: bpy.props.StringProperty(
name="URL",
description="Complete URL of shot on server that will be used to initiate strip",
default="",
)
_strip = None
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
if not sqe:
return False
strip = sqe.active_strip
return bool(
prefs.session_auth(context)
and cache.project_active_get()
and strip
and context.selected_sequences
and checkstrip.is_valid_type(strip)
)
def execute(self, context: bpy.types.Context) -> Set[str]:
shot_id = self._strip.kitsu.shot_id
# By url.
if self.use_url:
# http://192.168.178.80/productions/4dda1c36-1f49-44c7-98c9-93b40ea37dcd/shots/5e69e2e0-c3c8-4fc2-a4a3-f18151adf9dc
split = self.url.split("/")
shot_id = split[-1]
# By shot enum.
else:
shot_id = self._strip.kitsu.shot_id
if shot_id == "":
self.report({"WARNING"}, "Invalid selection. Please choose a shot")
return {"CANCELLED"}
# Check if id available on server (mainly for url option).
try:
shot = Shot.by_id(shot_id)
except (TypeError, gazu.exception.ServerErrorException):
self.report({"WARNING"}, "Invalid URL: %s" % self.url)
return {"CANCELLED"}
except gazu.exception.RouteNotFoundException:
self.report({"WARNING"}, "ID not found on server: %s" % shot_id)
return {"CANCELLED"}
seq = Sequence.by_id(shot.parent_id)
opsdata.link_metadata_strip(context, shot, seq, self._strip)
# Log.
t = "Linked strip: %s to shot: %s with ID: %s" % (
self._strip.name,
shot.name,
shot.id,
)
logger.info(t)
self.report({"INFO"}, t)
util.ui_redraw()
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
if context.window_manager.clipboard:
self.url = context.window_manager.clipboard
self._strip = context.scene.sequence_editor.active_strip
return context.window_manager.invoke_props_dialog(self, width=400) # type: ignore
def draw(self, context):
layout = self.layout
row = layout.row()
row.prop(self, "use_url")
if self.use_url:
row = layout.row()
row.prop(self, "url", text="")
else:
row = layout.row()
row.prop(self._strip.kitsu, "sequence_name")
row = layout.row()
row.prop(self._strip.kitsu, "shot_name")
row = layout.row()
class KITSU_OT_sqe_multi_edit_strip(bpy.types.Operator):
bl_idname = "kitsu.sqe_multi_edit_strip"
bl_label = "Multi Edit Strip"
bl_options = {"INTERNAL"}
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Multi edits shot name and sequence of selected sequence strips based on "
"settings in addon preferences with auto shot counter incrementation. "
"Useful to create a bulk of new shots"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
# Only if all selected strips are initialized but not linked
# and they all have the same sequence name.
sel_shots = context.selected_sequences
if not sel_shots:
cls.poll_message_set("No sequences are selected")
return False
nr_of_shots = len(sel_shots)
if nr_of_shots < 1:
cls.poll_message_set("Please more than one sequence")
return False
seq_name = sel_shots[0].kitsu.sequence_name
for s in sel_shots:
if s.kitsu.linked or not s.kitsu.initialized or not checkstrip.is_valid_type(s):
cls.poll_message_set(
"Please select unlinked, initialized strips, of either MOVIE or COLOR type"
)
return False
if s.kitsu.sequence_name != seq_name:
cls.poll_message_set(
"Strips have conflicting sequence names. Please select strips with the same sequence name, or no sequence"
)
return False
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(context)
shot_counter_increment = addon_prefs.shot_counter_increment
shot_counter_digits = addon_prefs.shot_counter_digits
shot_counter_start = context.window_manager.shot_counter_start
shot_pattern = addon_prefs.shot_pattern
strip = context.scene.sequence_editor.active_strip
sequence = context.window_manager.selected_sequence_name
var_project = (
addon_prefs.var_project_custom
if context.window_manager.var_use_custom_project
else context.window_manager.var_project_active
)
var_sequence = (
context.window_manager.var_sequence_custom
if context.window_manager.var_use_custom_seq
else sequence
)
succeeded = []
failed = []
logger.info("-START- Multi Edit Shot")
# Sort sequence after frame in.
selected_sequences = context.selected_sequences
selected_sequences = sorted(selected_sequences, key=lambda x: x.frame_final_start)
episode = cache.episode_active_get()
for idx, strip in enumerate(selected_sequences):
# Gen data for resolver.
counter_number = shot_counter_start + (shot_counter_increment * idx)
counter = str(counter_number).rjust(shot_counter_digits, "0")
var_lookup_table = {
"Episode": episode.name,
"Sequence": var_sequence,
"Project": var_project,
"Counter": counter,
}
# Run shot name resolver.
shot = opsdata.resolve_pattern(shot_pattern, var_lookup_table)
# Set metadata.
strip.kitsu.sequence_name = sequence
strip.kitsu.sequence_id = context.window_manager.selected_sequence_id
strip.kitsu.shot_name = shot
strip.name = shot
succeeded.append(strip)
logger.info(
"Strip: %s Assign sequence: %s Assign shot: %s" % (strip.name, sequence, shot)
)
# Report.
report_str = f"Assigned {len(succeeded)} Shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Multi Edit Shot")
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_pull_shot_meta(bpy.types.Operator):
"""
Operator that pulls metadata of all selected sequence strips from server
after performing various checks. Metadata will be saved in strip.kitsu.
"""
bl_idname = "kitsu.sqe_pull_shot_meta"
bl_label = "Pull Shot Metadata"
bl_options = {"INTERNAL"}
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Pulls metadata of all selected sequences from server. "
"This includes name, sequence, project and description. "
"Frame range information will not be pulled"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(prefs.session_auth(context))
def execute(self, context: bpy.types.Context) -> Set[str]:
succeeded = []
failed = []
logger.info("-START- Pulling shot metadata")
# Get sequences to process.
selected_sequences = context.selected_sequences
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
# Sort sequences.
selected_sequences = sorted(selected_sequences, key=lambda strip: strip.frame_final_start)
# Begin progress update.
context.window_manager.progress_begin(0, len(selected_sequences))
# Clear cache once.
Cache.clear_all()
# Track sequence ids that were processed to later update sequence.data["color"] on kitu.
sequence_ids: List[str] = []
# Shots.
for idx, strip in enumerate(selected_sequences):
context.window_manager.progress_update(idx)
if not checkstrip.is_valid_type(strip):
# Failed.append(strip).
continue
# Only if strip is linked to sevrer.
if not checkstrip.is_linked(strip):
# Failed.append(strip).
continue
# Check if shot is still available by id.
shot = checkstrip.shot_exists_by_id(strip, clear_cache=False)
if not shot:
failed.append(strip)
continue
# Push update to shot.
pull.shot_meta(strip, shot, clear_cache=False)
# Append sequence id.
if shot.parent_id not in sequence_ids:
sequence_ids.append(shot.parent_id)
succeeded.append(strip)
# End progress update.
context.window_manager.progress_update(len(selected_sequences))
context.window_manager.progress_end()
# Sequences.
# Begin second progress update for sequences.
context.window_manager.progress_begin(0, len(sequence_ids))
for idx, seq_id in enumerate(sequence_ids):
context.window_manager.progress_update(idx)
sequence = Sequence.by_id(seq_id)
opsdata.append_sequence_color(context, sequence)
# End second progress update.
context.window_manager.progress_update(len(sequence_ids))
context.window_manager.progress_end()
# Report.
report_str = f"Pulled metadata for {len(succeeded)} shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Pulling shot metadata")
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_uninit_strip(bpy.types.Operator):
bl_idname = "kitsu.sqe_uninit_strip"
bl_label = "Uninitialize"
bl_description = "Uninitialize selected strips. Only affects Sequence Editor. "
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Deletes all kitsu metadata of selected sequence strips. "
"It does not delete anything on the server"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(context.selected_sequences)
def execute(self, context: bpy.types.Context) -> Set[str]:
failed: List[bpy.types.Strip] = []
succeeded: List[bpy.types.Strip] = []
logger.info("-START- Uninitializing strips")
for strip in context.selected_sequences:
if not checkstrip.is_valid_type(strip):
continue
if not checkstrip.is_initialized(strip):
continue
if checkstrip.is_linked(strip):
continue
# Clear kitsu properties.
strip.kitsu.clear()
succeeded.append(strip)
logger.info("Uninitialized strip: %s", strip.name)
# Report.
report_str = f"Uninitialized {len(succeeded)} strips"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Uninitializing strips")
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_unlink_shot(bpy.types.Operator):
bl_idname = "kitsu.sqe_unlink_shot"
bl_label = "Unlink"
bl_description = (
"Deletes link to the server for each selected sequence strip. "
"Keeps some metadata. "
"It does not change anything on the server"
)
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(context.selected_sequences)
def execute(self, context: bpy.types.Context) -> Set[str]:
failed: List[bpy.types.Strip] = []
succeeded: List[bpy.types.Strip] = []
logger.info("-START- Unlinking shots")
for strip in context.selected_sequences:
if not checkstrip.is_valid_type(strip):
continue
if not checkstrip.is_initialized(strip):
continue
if not checkstrip.is_linked(strip):
continue
# Clear kitsu properties.
shot_name = strip.kitsu.shot_name
strip.kitsu.unlink()
succeeded.append(strip)
logger.info("Unlinked shot: %s", shot_name)
# Report.
report_str = f"Unlinked {len(succeeded)} shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Unlinking shots")
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_push_del_shot(bpy.types.Operator):
bl_idname = "kitsu.sqe_push_del_shot"
bl_label = "Delete Shot"
bl_description = "Deletes shot on server and clears metadata of selected sequence strips"
confirm: bpy.props.BoolProperty(name="Confirm")
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(prefs.session_auth(context) and context.selected_sequences)
def execute(self, context: bpy.types.Context) -> Set[str]:
if not self.confirm:
self.report({"WARNING"}, "Push delete aborted")
return {"CANCELLED"}
succeeded = []
failed = []
logger.info("-START- Deleting shots")
# Clear cache.
Cache.clear_all()
# Begin progress update.
selected_sequences = context.selected_sequences
context.window_manager.progress_begin(0, len(selected_sequences))
for idx, strip in enumerate(selected_sequences):
context.window_manager.progress_update(idx)
if not checkstrip.is_valid_type(strip):
continue
# Check if strip is already linked to sevrer.
if not checkstrip.is_linked(strip):
continue
# Check if shot still exists to sevrer.
shot = checkstrip.shot_exists_by_id(strip, clear_cache=False)
if not shot:
failed.append(strip)
continue
# Delete shot.
push.delete_shot(strip, shot) # This clears all kitsu properties.
succeeded.append(strip)
# End progress update.
context.window_manager.progress_update(len(selected_sequences))
context.window_manager.progress_end()
# Report.
report_str = f"Deleted {len(succeeded)} shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Deleting shots")
util.ui_redraw()
return {"FINISHED"}
def invoke(self, context, event):
self.confirm = False
return context.window_manager.invoke_props_dialog(self, width=500)
def draw(self, context):
layout = self.layout
col = layout.column()
selshots = context.selected_sequences
strips_to_delete = [s for s in selshots if s.kitsu.linked]
if len(selshots) > 1:
noun = "%i shots" % len(strips_to_delete)
else:
noun = "this shot"
col.prop(
self,
"confirm",
text="Delete %s on server" % noun,
)
class KITSU_OT_sqe_set_thumbnail_task_type(bpy.types.Operator):
bl_idname = "kitsu.set_thumbnail_task_type"
bl_label = "Set Thumbnail Task Type"
bl_options = {"INTERNAL"}
bl_property = "enum_prop"
bl_description = "Sets kitsu task type that will be used when uploading thumbnails"
enum_prop: bpy.props.EnumProperty(items=cache.get_shot_task_types_enum) # type: ignore
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(prefs.session_auth(context) and cache.project_active_get())
def execute(self, context: bpy.types.Context) -> Set[str]:
# Task type selected by user.
task_type_id = self.enum_prop
if not task_type_id:
return {"CANCELLED"}
task_type = TaskType.by_id(task_type_id)
# Update scene properties.
context.scene.kitsu.task_type_thumbnail_name = task_type.name
context.scene.kitsu.task_type_thumbnail_id = task_type_id
util.ui_redraw()
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class KITSU_OT_sqe_set_sqe_render_task_type(bpy.types.Operator):
bl_idname = "kitsu.set_sqe_render_task_type"
bl_label = "Set Sqe Render Task Type"
bl_options = {"INTERNAL"}
bl_property = "enum_prop"
bl_description = "Sets kitsu task type that will be used when uploading sequence editor renders"
enum_prop: bpy.props.EnumProperty(items=cache.get_shot_task_types_enum) # type: ignore
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(prefs.session_auth(context) and cache.project_active_get())
def execute(self, context: bpy.types.Context) -> Set[str]:
# Task type selected by user.
task_type_id = self.enum_prop
if not task_type_id:
return {"CANCELLED"}
task_type = TaskType.by_id(task_type_id)
# Update scene properties.
context.scene.kitsu.task_type_sqe_render_name = task_type.name
context.scene.kitsu.task_type_sqe_render_id = task_type_id
util.ui_redraw()
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
class KITSU_OT_sqe_push_render_still(bpy.types.Operator):
bl_idname = "kitsu.seq_render_still"
bl_label = "Push Still"
bl_options = {"INTERNAL"}
bl_description = (
"Makes and saves one still for each shot. "
"Uploads each still to server as a preview image for the selected task type"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(prefs.session_auth(context) and context.scene.kitsu.task_type_thumbnail_id)
def execute(self, context: bpy.types.Context) -> Set[str]:
nr_of_strips: int = len(context.selected_sequences)
do_multishot: bool = nr_of_strips > 1
failed = []
upload_queue: List[Path] = [] # Will be used as succeeded list
# Get task type by id from user selection enum property.
task_type = TaskType.by_id(context.scene.kitsu.task_type_thumbnail_id)
logger.info("-START- Pushing shot thumbnails")
# Clear cache.
Cache.clear_all()
with self.override_render_settings(context):
with self.temporary_current_frame(context) as original_curframe:
# ----RENDER AND SAVE THUMBNAILS ------.
# Begin first progress update.
selected_sequences = context.selected_sequences
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
# Sort sequences.
selected_sequences = sorted(
selected_sequences, key=lambda strip: strip.frame_final_start
)
context.window_manager.progress_begin(0, len(selected_sequences))
for idx, strip in enumerate(selected_sequences):
context.window_manager.progress_update(idx)
if not checkstrip.is_valid_type(strip):
# Failed.append(strip).
continue
# Only if strip is linked to sevrer.
if not checkstrip.is_linked(strip):
# Failed.append(strip).
continue
# Check if shot is still available by id.
shot = checkstrip.shot_exists_by_id(strip, clear_cache=False)
if not shot:
failed.append(strip)
continue
# If only one strip is selected,.
if not do_multishot:
# If active strip is not contained in the current frame, use middle frame of active strip
# otherwise don't change frame and use current one.
if not checkstrip.contains(strip, original_curframe):
self.set_middle_frame(context, strip)
else:
self.set_middle_frame(context, strip)
path = self.make_thumbnail(context, strip)
upload_queue.append(path)
# End first progress update.
context.window_manager.progress_update(len(upload_queue))
context.window_manager.progress_end()
# ----ULPOAD THUMBNAILS ------.
# Begin second progress update.
context.window_manager.progress_begin(0, len(upload_queue))
# Process thumbnail queue.
for idx, filepath in enumerate(upload_queue):
context.window_manager.progress_update(idx)
opsdata.upload_preview(context, filepath, task_type, comment="Update thumbnail")
# End second progress update.
context.window_manager.progress_update(len(upload_queue))
context.window_manager.progress_end()
# Report.
report_str = f"Created thumbnails for {len(upload_queue)} shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Pushing shot thumbnails")
return {"FINISHED"}
def make_thumbnail(self, context: bpy.types.Context, strip: bpy.types.Strip) -> Path:
bpy.ops.render.render()
file_name = f"{strip.kitsu.shot_id}_{str(context.scene.frame_current)}.jpg"
path = self._save_render(bpy.data.images["Render Result"], file_name)
logger.info(f"Saved thumbnail of shot {strip.kitsu.shot_name} to {path.as_posix()}")
return path
def _save_render(self, datablock: bpy.types.Image, file_name: str) -> Path:
"""Save the current render image to disk"""
addon_prefs = prefs.addon_prefs_get(bpy.context)
folder_name = addon_prefs.thumbnail_dir
# Ensure folder exists.
folder_path = Path(folder_name).absolute()
folder_path.mkdir(parents=True, exist_ok=True)
path = folder_path.joinpath(file_name)
datablock.save_render(str(path))
return path.absolute()
@contextlib.contextmanager
def override_render_settings(self, context, thumbnail_width=512):
"""Overrides the render settings for thumbnail size in a 'with' block scope"""
rd = context.scene.render
# Remember current render settings in order to restore them later.
percentage = rd.resolution_percentage
file_format = rd.image_settings.file_format
quality = rd.image_settings.quality
use_stamp_frame = rd.use_stamp_frame
try:
# Set the render settings to thumbnail size.
# Update resolution % instead of the actual resolution to scale text strips properly.
rd.resolution_percentage = round(thumbnail_width * 100 / rd.resolution_x)
rd.image_settings.file_format = "JPEG"
rd.image_settings.quality = 80
rd.use_stamp_frame = False
yield
finally:
# Return the render settings to normal.
rd.resolution_percentage = percentage
rd.image_settings.file_format = file_format
rd.image_settings.quality = quality
rd.use_stamp_frame = use_stamp_frame
@contextlib.contextmanager
def temporary_current_frame(self, context):
"""Allows the context to set the scene current frame, restores it on exit.
Yields the initial current frame, so it can be used for reference in the context.
"""
current_frame = context.scene.frame_current
try:
yield current_frame
finally:
context.scene.frame_current = current_frame
@staticmethod
def set_middle_frame(
context: bpy.types.Context,
strip: bpy.types.Strip,
) -> int:
"""Sets the current frame to the middle frame of the strip"""
middle = round((strip.frame_final_start + strip.frame_final_end) / 2)
context.scene.frame_set(middle)
return middle
class KITSU_OT_sqe_push_render(bpy.types.Operator):
bl_idname = "kitsu.sqe_push_render"
bl_label = "Push Render"
bl_options = {"INTERNAL"}
bl_description = (
"Makes and saves a .mp4 for each shot. "
"Uploads each render on server as a preview image for the selected task type"
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return bool(prefs.session_auth(context) and context.scene.kitsu.task_type_sqe_render_id)
def execute(self, context: bpy.types.Context) -> Set[str]:
failed = []
upload_queue: List[Path] = [] # will be used as successed list
# Get task stype by id from user selection enum property.
task_type = TaskType.by_id(context.scene.kitsu.task_type_sqe_render_id)
logger.info("-START- Pushing Sequence Render")
# Clear cache.
Cache.clear_all()
with self.override_render_settings(context):
# ----RENDER AND SAVE SQE ------.
# Get strips.
selected_sequences = context.selected_sequences
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
# Sort strips.
selected_sequences = sorted(
selected_sequences, key=lambda strip: strip.frame_final_start
)
# Begin first progress update.
context.window_manager.progress_begin(0, len(selected_sequences))
for idx, strip in enumerate(selected_sequences):
context.window_manager.progress_update(idx)
if not checkstrip.is_valid_type(strip):
continue
# Only if strip is linked to sevrer.
if not checkstrip.is_linked(strip):
continue
# Check if shot is still available by id.
shot = checkstrip.shot_exists_by_id(strip, clear_cache=False)
if not shot:
failed.append(strip)
continue
# Output path.
output_path = self._gen_output_path(strip, task_type)
context.scene.render.filepath = output_path.as_posix()
# Frame range.
context.scene.frame_start = strip.frame_final_start
context.scene.frame_end = strip.frame_final_end - 1
# Ensure folder exists.
output_path.parent.mkdir(parents=True, exist_ok=True)
# Make opengl render.
bpy.ops.render.opengl(animation=True, sequencer=True)
# Append path to upload queue.
upload_queue.append(output_path)
# End first progress update.
context.window_manager.progress_update(len(upload_queue))
context.window_manager.progress_end()
# ----UPLOAD SQE RENDER ------.
# Begin second progress update.
context.window_manager.progress_begin(0, len(upload_queue))
# Process thumbnail queue.
for idx, filepath in enumerate(upload_queue):
context.window_manager.progress_update(idx)
opsdata.upload_preview(context, filepath, task_type, comment="Sequence Editor Render")
# End second progress update.
context.window_manager.progress_update(len(upload_queue))
context.window_manager.progress_end()
# Report.
report_str = f"Uploaded sequence editor render for {len(upload_queue)} shots"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Pushing Sequence Editor Render")
return {"FINISHED"}
def _gen_output_path(self, strip: bpy.types.Strip, task_type: TaskType) -> Path:
addon_prefs = prefs.addon_prefs_get(bpy.context)
folder_name = addon_prefs.sqe_render_dir
file_name = f"{strip.kitsu.shot_id}_{strip.kitsu.shot_name}.{(task_type.name).lower()}.mp4"
return Path(folder_name).absolute().joinpath(file_name)
@contextlib.contextmanager
def override_render_settings(self, context, thumbnail_width=256):
"""Overrides the render settings for thumbnail size in a 'with' block scope"""
rd = context.scene.render
# Remember current render settings in order to restore them later.
# Filepath.
filepath = rd.filepath
# Format render settings.
percentage = rd.resolution_percentage
file_format = rd.image_settings.file_format
ffmpeg_constant_rate = rd.ffmpeg.constant_rate_factor
ffmpeg_codec = rd.ffmpeg.codec
ffmpeg_format = rd.ffmpeg.format
ffmpeg_audio_codec = rd.ffmpeg.audio_codec
# Scene settings.
use_preview_range = context.scene.use_preview_range
frame_start = context.scene.frame_start
frame_end = context.scene.frame_end
current_frame = context.scene.frame_current
try:
# Format render settings.
rd.resolution_percentage = 100
rd.image_settings.file_format = "FFMPEG"
rd.ffmpeg.constant_rate_factor = "MEDIUM"
rd.ffmpeg.codec = "H264"
rd.ffmpeg.format = "MPEG4"
rd.ffmpeg.audio_codec = "AAC"
# Scene settings.
context.scene.use_preview_range = False
yield
finally:
# Filepath.
rd.filepath = filepath
# Return the render settings to normal.
rd.resolution_percentage = percentage
rd.image_settings.file_format = file_format
rd.ffmpeg.codec = ffmpeg_codec
rd.ffmpeg.constant_rate_factor = ffmpeg_constant_rate
rd.ffmpeg.format = ffmpeg_format
rd.ffmpeg.audio_codec = ffmpeg_audio_codec
# Scene settings.
context.scene.frame_start = frame_start
context.scene.frame_end = frame_end
context.scene.frame_current = current_frame
context.scene.use_preview_range = use_preview_range
class KITSU_OT_sqe_push_shot(bpy.types.Operator):
bl_idname = "kitsu.sqe_push_shot"
bl_label = "Push Shot to Kitsu"
bl_description = "Pushes the active strip to Kitsu"
comment: bpy.props.StringProperty(
name="Comment",
description="Comment that will be appended to this video on Kitsu",
default="",
)
task_type: bpy.props.EnumProperty(
name="Task Type",
description="Which task this video should be added to",
items=cache.get_task_types_enum_for_current_context,
)
task_status: bpy.props.EnumProperty(
name="Task Status",
description="What to set the task's status to",
items=cache.get_all_task_statuses_enum,
)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
active_strip = context.scene.sequence_editor.active_strip
if not hasattr(active_strip, 'filepath'):
cls.poll_message_set("Selected Strip is not a Video")
return False
return bool(prefs.session_auth(context))
def invoke(self, context, _event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(self, 'task_type')
layout.prop(self, 'task_status')
layout.prop(self, 'comment')
def execute(self, context: bpy.types.Context) -> Set[str]:
active_strip = context.scene.sequence_editor.active_strip
# Find the metadata strip of this strip that contains Kitsu information
# about what sequence and shot this strip belongs to.
shot_name = active_strip.name.split(bkglobals.DELIMITER)[0]
metadata_strip = context.scene.sequence_editor.sequences.get(shot_name)
if not metadata_strip:
# The metadata strip should've been created by sqe_create_review_session,
# if the Kitsu integration is enabled in the add-on preferences,
# the Kitsu add-on is enabled, and valid Kitsu credentials were entered.
self.report({"ERROR"}, f"Could not find Kitsu metadata strip: {shot_name}.")
return {"CANCELLED"}
if not self.task_status:
self.report({"ERROR"}, "Failed to create playblast. Missing task status")
return {"CANCELLED"}
# Set the Kitsu sequence and shot information in the context
cache.sequence_active_set_by_id(context, metadata_strip.kitsu.sequence_id)
cache.shot_active_set_by_id(context, metadata_strip.kitsu.shot_id)
# Upload render
self.report({"INFO"}, f"Trying to upload render for {shot_name}")
self._upload_render(context, Path(bpy.path.abspath(active_strip.filepath)))
self.report({"INFO"}, f"Uploaded render for {shot_name}")
return {'FINISHED'}
def _upload_render(self, context: bpy.types.Context, filepath: Path) -> None:
# Get shot.
shot = cache.shot_active_get()
# Get task status and type.
task_status = TaskStatus.by_id(self.task_status)
task_type = TaskType.by_id(self.task_type)
if not task_type:
raise RuntimeError(
f"Failed to upload playblast. Task type: {self.task_type} was not found"
)
# Find / get latest task
task = Task.by_name(shot, task_type)
if not task:
# An Entity on the server can have 0 tasks even tough task types exist.
# We have to create a task first before being able to upload a thumbnail.
task = Task.new_task(shot, task_type, task_status=task_status)
# Create a comment
comment = task.add_comment(
task_status,
comment=self.comment,
)
# Add_preview_to_comment
task.add_preview_to_comment(comment, filepath.as_posix())
logger.info(f"Uploaded render for shot: {shot.name} under: {task_type.name}")
class KITSU_OT_sqe_debug_duplicates(bpy.types.Operator):
bl_idname = "kitsu.sqe_debug_duplicates"
bl_label = "Debug Duplicates"
bl_property = "duplicates"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Searches for sequence strips that are linked to the same "
"shot id. Shows them in a drop down menu which triggers a selection"
)
duplicates: bpy.props.EnumProperty(items=opsdata.sqe_get_duplicates, name="Duplicates")
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
strip_name = self.duplicates
if not strip_name:
return {"CANCELLED"}
# Deselect all if something is selected.
if context.selected_sequences:
bpy.ops.sequencer.select_all()
strip = context.scene.sequence_editor.sequences_all[strip_name]
strip.select = True
bpy.ops.sequencer.select()
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
opsdata._sqe_duplicates[:] = opsdata.sqe_update_duplicates(context)
return context.window_manager.invoke_props_popup(self, event) # type: ignore
class KITSU_OT_sqe_debug_not_linked(bpy.types.Operator):
bl_idname = "kitsu.sqe_debug_not_linked"
bl_label = "Debug Not Linked"
bl_property = "not_linked"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Searches for sequence strips that are initialized but not linked yet. "
"Shows them in a drop down menu which triggers a selection"
)
not_linked: bpy.props.EnumProperty(items=opsdata.sqe_get_not_linked, name="Not Linked")
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
strip_name = self.not_linked
if not strip_name:
return {"CANCELLED"}
# Deselect all if something is selected.
if context.selected_sequences:
bpy.ops.sequencer.select_all()
strip = context.scene.sequence_editor.sequences_all[strip_name]
strip.select = True
bpy.ops.sequencer.select()
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
opsdata._sqe_not_linked[:] = opsdata.sqe_update_not_linked(context)
return context.window_manager.invoke_props_popup(self, event) # type: ignore
class KITSU_OT_sqe_debug_multi_project(bpy.types.Operator):
bl_idname = "kitsu.sqe_debug_multi_project"
bl_label = "Debug Multi Projects"
bl_property = "multi_project"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Searches for sequence strips that come from different projects. "
"Shows them in a drop down menu which triggers a selection"
)
multi_project: bpy.props.EnumProperty(items=opsdata.sqe_get_multi_project, name="Multi Project")
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
strip_name = self.multi_project
if not strip_name:
return {"CANCELLED"}
# Deselect all if something is selected.
if context.selected_sequences:
bpy.ops.sequencer.select_all()
strip = context.scene.sequence_editor.sequences_all[strip_name]
strip.select = True
bpy.ops.sequencer.select()
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
opsdata._sqe_multi_project[:] = opsdata.sqe_update_multi_project(context)
return context.window_manager.invoke_props_popup(self, event) # type: ignore
class KITSU_OT_sqe_pull_edit(bpy.types.Operator):
bl_idname = "kitsu.sqe_pull_edit"
bl_label = "Pull Edit"
bl_description = (
"Pulls the entire edit from kitsu and creates a metadata strip for each shot. "
"Does not change existing strips. Only places new strips if there is space"
)
bl_options = {"REGISTER", "UNDO", "UNDO_GROUPED"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
addon_prefs = prefs.addon_prefs_get(context)
return bool(prefs.session_auth(context) and cache.project_active_get())
def execute(self, context: bpy.types.Context) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(context)
failed = []
created = []
succeeded = []
existing = []
channel = context.scene.kitsu.pull_edit_channel
active_project = cache.project_active_get()
active_episode = cache.episode_active_get()
sequences = (
active_episode.get_sequences_all()
if active_episode
else active_project.get_sequences_all()
)
shot_strips = checksqe.get_shot_strips(context)
occupied_ranges = checksqe.get_occupied_ranges(context)
all_shots = active_project.get_shots_all()
selection = context.selected_sequences
logger.info("-START- Pulling Edit")
# Begin progress update.
context.window_manager.progress_begin(0, len(all_shots))
progress_idx = 0
# Process sequence after sequence.
for seq in sequences:
print("\n" * 2)
logger.info("Processing Sequence %s", seq.name)
shots = seq.get_all_shots()
# Extend context.scene.kitsu.sequence_colors property.
opsdata.append_sequence_color(context, seq)
# Process all shots for sequence.
for shot in shots:
context.window_manager.progress_update(progress_idx)
progress_idx += 1
# Can happen, propably when shot is missing frame information on
# kitsu.
if not shot.data:
logger.warning(
"Shot %s, is missing 'data' dictionary. Can't determine frame_in and frame_out. Skip.",
shot.name,
)
continue
# Get frame range information.
frame_start = shot.data.get("frame_in", None)
frame_end = shot.data.get("frame_out", None)
# Continue if frame range information is missing.
if frame_start is None or frame_end is None:
failed.append(shot)
logger.error(
"Failed to create shot %s. Missing frame range information",
shot.name,
)
continue
# Frame info comes in str format from kitsu.
frame_start = int(frame_start)
frame_end = int(frame_end)
shot_range = range(frame_start, frame_end + 1)
# Try to find existing strip that is already linked to that shot.
strip = self._find_shot_strip(shot_strips, shot.id)
# Check if on the specified channel there is space to put the strip.
if str(channel) in occupied_ranges:
if checksqe.is_range_occupied(shot_range, occupied_ranges[str(channel)]):
failed.append(shot)
logger.error(
"Failed to create shot %s. Channel: %i Range: %i - %i is occupied",
shot.name,
channel,
frame_start,
frame_end,
)
continue
# TODO Refactor as this reuses code from KITSU_OT_sqe_create_metadata_strip
if not strip:
# Create new strip.
strip = opsdata.create_metadata_strip(
context.scene, shot.name, channel, frame_start, frame_end
)
# Apply slip to match offset.
self._apply_strip_slip_from_shot(context, strip, shot)
created.append(shot)
logger.info("Shot %s created new strip", shot.name)
else:
# Update properties of existing strip.
strip.channel = channel
logger.info("Shot %s use existing strip: %s", shot.name, strip.name)
existing.append(strip)
# Set blend alpha.
strip.blend_alpha = 0
# Pull shot meta and link shot.
pull.shot_meta(strip, shot, clear_cache=False)
succeeded.append(shot)
# End progress update.
context.window_manager.progress_update(len(all_shots))
context.window_manager.progress_end()
# Restore selection.
if context.selected_sequences:
bpy.ops.sequencer.select_all()
for s in selection:
s.select = True
# Report.
report_str = f"Shots: Succeded:{len(succeeded)} | Created {len(created)} | Existing: {len(existing)}"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Pulling Edit")
return {"FINISHED"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self, width=300) # type: ignore
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text="Set channel in which the entire edit should be created")
row = layout.row()
row.prop(context.scene.kitsu, "pull_edit_channel")
def _find_shot_strip(
self, shot_strips: List[bpy.types.Strip], shot_id: str
) -> Optional[bpy.types.Strip]:
for strip in shot_strips:
if strip.kitsu.shot_id == shot_id:
return strip
return None
def _get_random_pastel_color_rgb(self) -> Tuple[float, float, float]:
"""Returns a randomly generated color with high brightness and low saturation"""
hue = random.random()
saturation = random.uniform(0.25, 0.33)
brightness = random.uniform(0.75, 0.83)
color = colorsys.hsv_to_rgb(hue, saturation, brightness)
return (color[0], color[1], color[2])
def _apply_strip_slip_from_shot(
self, context: bpy.types.Context, strip: bpy.types.Strip, shot: Shot
) -> None:
# get offset
offset = strip.kitsu_3d_start - int(shot.get_3d_start())
# Deselect everything.
if context.selected_sequences:
bpy.ops.sequencer.select_all()
# Select strip and run slip op.
strip.select = True
bpy.ops.sequencer.slip(offset=offset)
class KITSU_OT_sqe_init_strip_start_frame(bpy.types.Operator):
bl_idname = "kitsu.sqe_init_strip_start_frame"
bl_label = "Initialize Shot Start Frame"
bl_description = "Calculates offset so the current shot starts at 101"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return True
def execute(self, context: bpy.types.Context) -> Set[str]:
succeeded = []
failed = []
logger.info("-START- Initializing strip start frame")
selected_sequences = context.selected_sequences
if not selected_sequences:
selected_sequences = context.scene.sequence_editor.sequences_all
for strip in selected_sequences:
if not checkstrip.is_valid_type(strip):
continue
if not strip.kitsu.initialized:
logger.info("%s not initialized", strip.name)
continue
# Apply strip.kitsu.frame_start_offset.
opsdata.init_start_frame_offset(strip)
succeeded.append(strip)
logger.info(
"%s initiated start frame to 101 by applying offset: %i ",
strip.name,
strip.kitsu.frame_start_offset,
)
# Report.
report_str = f"Initiated start frame of {len(succeeded)} strips"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Log.
logger.info("-END- Initializing strip start frame")
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_create_metadata_strip(bpy.types.Operator):
bl_idname = "kitsu.sqe_create_metadata_strip"
bl_label = "Create Metadata Strip"
bl_description = (
"Adds metadata strip for each selected strip. "
"Tries to place metadata strip one channel above selected "
)
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
addon_prefs = prefs.addon_prefs_get(context)
return bool(context.selected_sequences)
def execute(self, context: bpy.types.Context) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(context)
failed = []
created = []
occupied_ranges = checksqe.get_occupied_ranges(context)
logger.info("-START- Creating Metadata Strips")
selected_sequences = context.selected_sequences
# Check if metadata strip file actually exists.
for strip in selected_sequences:
# Get frame range information from current strip.
strip_range = range(strip.frame_final_start, strip.frame_final_end)
channel = strip.channel + 1
# Check if one channel above strip there is space to put the metadata strip.
if str(channel) in occupied_ranges:
if checksqe.is_range_occupied(strip_range, occupied_ranges[str(channel)]):
failed.append(strip)
logger.error(
"Failed to create metadata strip for %s. Channel: %i Range: %i - %i is occupied",
strip.name,
channel,
strip.frame_final_start,
strip.frame_final_end,
)
continue
# Create new metadata strip.
# TODO: frame range of metadata strip is 1000 which is problematic because it needs to fit
# on the first try, EDIT: seems to work maybe per python overlaps of sequences possible?
metadata_strip = opsdata.create_metadata_strip(
context.scene,
f"{strip.name}{bkglobals.DELIMITER}metadata{bkglobals.SPACE_REPLACER}strip",
strip.channel + 1,
strip.frame_final_start,
strip.frame_final_end,
)
created.append(metadata_strip)
logger.info(
"%s created metadata strip: %s",
strip.name,
metadata_strip.name,
)
# Report.
report_str = f"Created {len(created)} metadata strips"
report_state = "INFO"
if failed:
report_state = "WARNING"
report_str += f" | Failed: {len(failed)}"
self.report(
{report_state},
report_str,
)
# Deselect source strips after creating Metadata strips
for strip in selected_sequences:
if strip not in created:
strip.select = False
# Log.
logger.info("-END- Creating Metadata Strips")
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_fix_metadata_strips(bpy.types.Operator):
bl_idname = "kitsu.sqe_fix_metadata_strip"
bl_label = "Fix Metadata Strip Missing Media"
bl_description = "Fixes missing media error in metadata strips. "
bl_options = {"REGISTER", "UNDO"}
def execute(self, context: Context):
addon_prefs = prefs.addon_prefs_get(context)
if len(context.selected_sequences) == 0:
all_strips = context.scene.sequence_editor.sequences
else:
all_strips = context.selected_sequences
offline_metadata_strips = [
strip
for strip in all_strips
if strip.kitsu.shot_id != '' and not Path(strip.filepath).is_file()
]
for strip in offline_metadata_strips:
strip.filepath = addon_prefs.metadatastrip_file
return {"FINISHED"}
class KITSU_OT_sqe_add_sequence_color(bpy.types.Operator):
"""
Adds sequence of active strip to scene.kitsu.sequence_colors collection property
"""
bl_idname = "kitsu.add_sequence_color"
bl_label = "Add Sequence Color"
bl_description = "Registers a sequence color for active sequence strip"
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
if not sqe:
return False
active_strip = sqe.active_strip
return bool(active_strip and active_strip.kitsu.sequence_id)
def execute(self, context: bpy.types.Context) -> Set[str]:
sequence_colors = context.scene.kitsu.sequence_colors
active_strip = context.scene.sequence_editor.active_strip
sequence_id = active_strip.kitsu.sequence_id
# Check if sequence_id is already in sequence color collection property
if sequence_id in sequence_colors.keys():
self.report(
{"WARNING"},
f"Sequence {sequence_id} (ID: {active_strip.kitsu.sequence_name}) already in scene.kitsu.sequence_colors",
)
return {"CANCELLED"}
# Add new item to sequence color collection property
item = context.scene.kitsu.sequence_colors.add()
item.name = active_strip.kitsu.sequence_id
self.report(
{"INFO"},
f"Added {sequence_id} (ID: {active_strip.kitsu.sequence_name}) to scene.kitsu.seqeuence_colors",
)
return {"FINISHED"}
class KITSU_OT_sqe_scan_for_media_updates(bpy.types.Operator):
bl_idname = "kitsu.sqe_scan_for_media_updates"
bl_label = "Scan for media updates"
bl_description = (
"Scans sequence editor for movie strips and highlights them if there is a more recent version of their source media. "
"Source Media is located either in the Playblast Directory or the 'Media Update Search Paths' directories"
)
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
if not sqe:
return False
return bool(sqe.sequences_all)
def execute(self, context: bpy.types.Context) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(context)
outdated: List[bpy.types.Strip] = []
invalid: List[bpy.types.Strip] = []
no_version: List[bpy.types.Strip] = []
up_to_date: List[bpy.types.Strip] = []
checked: List[bpy.types.Strip] = []
excluded: List[bpy.types.Strip] = []
sequences = context.selected_sequences
if not sequences:
sequences = context.scene.sequence_editor.sequences_all
logger.info("-START- Scanning for media updates")
for strip in sequences:
if not strip.type == "MOVIE":
continue
checked.append(strip)
# Check if it has valid filepath key.
if not strip.filepath:
logger.info("%s has invalid filepath. Skip", strip.name)
invalid.append(strip)
continue
media_path_old = Path(os.path.abspath(bpy.path.abspath(strip.filepath)))
current_version = util.get_version(media_path_old.name)
# Check if filepath is in include path.
included = False
paths = [item.filepath for item in addon_prefs.media_update_search_paths]
paths.append(addon_prefs.shot_playblast_root_dir)
for path in paths:
filepath = Path(os.path.abspath(bpy.path.abspath(path)))
if media_path_old.as_posix().startswith(filepath.as_posix()):
included = True
break
if not included:
logger.info("Not included in media update search list: %s", strip.filepath)
excluded.append(strip)
continue
# Check if source media path contains version string.
if not current_version:
no_version.append(strip)
continue
# Gather valid files to compare source media to.
media_folder = media_path_old.parent
media_all: List[Path] = [
f for f in media_folder.iterdir() if f.is_file() and util.get_version(f.name)
]
# List of files that are named as source except for version str.
valid_files: List[Path] = []
for file in media_all:
# Version should exists here, we already check for that in list comprehension.
version = util.get_version(file.name)
# We only want to consider files that have the same name except for version string.
if file.name.replace(version, "") != media_path_old.name.replace(
current_version, ""
):
continue
valid_files.append(file)
valid_files.sort(reverse=True)
# No valid files found, should not happen source file should be at least here.
if not valid_files:
continue
if valid_files[0] == media_path_old:
# Logger.info("%s already up to date: %s", strip.name, strip.filepath).
strip.kitsu.media_outdated = False
up_to_date.append(strip)
continue
# Load latest media.
logger.info(
"%s newer version of source media available: %s > %s",
strip.name,
current_version,
util.get_version(valid_files[0].name),
)
# Append to outdated list.
outdated.append(strip)
# Set media outdatet property for gpu overlay.
strip.kitsu.media_outdated = True
# Report.
self.report(
{"INFO"},
f"Scanned {len(checked)} | Outdatet: {len(outdated)} | Up-to-date: {len(up_to_date)} | Invalid: {len(invalid) + len(excluded) + len(no_version)}",
)
# Log.
logger.info("-END- Scanning for outdated media")
util.ui_redraw()
return {"FINISHED"}
def get_used_channels(self: Any, context: bpy.types.Context, edit_text: str = "") -> List[str]:
used_channels = []
for seq in context.scene.sequence_editor.sequences_all:
used_channels.append(seq.channel)
aval_channels = []
for channel in range(1, 100):
if channel not in used_channels:
aval_channels.append(channel)
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)
# Convert Sets to Lists for easy access to contents
succeeded = list(succeeded)
failed = list(failed)
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(
name="Channel",
description="Choose an empty target channel to place image sequences onto",
search=get_used_channels,
search_options={'SORT'},
)
file_type: bpy.props.EnumProperty(
name="File Type",
description="Choose an empty target channel to place image sequences onto",
items=[
('.jpg', 'JPG', '', '', 0),
('.exr', 'EXR', '', '', 1),
],
)
set_color_space: bpy.props.BoolProperty(
name="Match Scene Color",
description="Match scene color space to image sequence file type. \n JPG: sGRB with no look, EXR: Linear with Medium Contrast look",
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
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 a 'MOVIE' strip")
return False
return True
def set_scene_colorspace(self, context):
if bpy.app.version_string.split('.')[0] == '3':
color_space_name = "Linear"
else:
color_space_name = "Linear Rec.709"
scene = context.scene
if self.file_type == ".jpg":
scene.sequencer_colorspace_settings.name = "sRGB"
scene.view_settings.look = "None"
scene.view_settings.view_transform = 'Standard'
if self.file_type == ".exr":
scene.sequencer_colorspace_settings.name = color_space_name
scene.view_settings.look = 'Medium High Contrast'
scene.view_settings.view_transform = 'Filmic'
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, "task_type")
layout.prop(self, "channel_selection")
layout.prop(self, "file_type")
layout.prop(self, "set_color_space")
def get_shot_name(self, strip):
filepath = strip.filepath
name = Path(filepath).name
match = re.search(r"^(\w+)-", name)
if match:
return match.group(1)
return
def import_strip(self, context, metadata_strip, directory, channel):
# https://blender.stackexchange.com/questions/286946/how-to-add-image-sequence-in-sequencer-via-python
frame_start = metadata_strip.frame_final_start
frame_end = metadata_strip.frame_final_end
files = []
shot = Shot.by_id(metadata_strip.kitsu.shot_id)
start_frame = (
shot.data.get('3d_start') if shot.data.get('3d_start') else bkglobals.FRAME_START
)
for file in sorted(list(directory.iterdir())):
if file.name.endswith("mp4"):
continue
frame_number = int(file.name.split(".")[0])
duration = metadata_strip.frame_duration + start_frame
if self.file_type in file.name and frame_number < duration:
files.append({"name": file.name})
area_type = 'SEQUENCE_EDITOR'
areas = [area for area in bpy.context.window.screen.areas if area.type == area_type]
if files == []:
self.report({'ERROR'}, "No files found")
return {'CANCELLED'}
len_strip = len(context.scene.sequence_editor.sequences_all)
with bpy.context.temp_override(
window=bpy.context.window,
area=areas[0],
regions=[region for region in areas[0].regions if region.type == 'WINDOW'][0],
screen=bpy.context.window.screen,
):
bpy.ops.sequencer.image_strip_add(
directory=directory._str,
files=files,
relative_path=True,
show_multiview=False,
frame_start=frame_start,
frame_end=frame_end,
channel=channel,
fit_method='FIT',
)
if len_strip + 1 != len(context.scene.sequence_editor.sequences_all):
print(f"Failed to import image sequence for {metadata_strip.name}")
return
new_strip = context.selected_sequences[0]
new_strip.animation_offset_end = metadata_strip.animation_offset_end
new_strip.animation_offset_start = metadata_strip.animation_offset_start
new_strip.frame_offset_end = metadata_strip.frame_offset_end
new_strip.frame_offset_start = metadata_strip.frame_offset_start
new_strip.frame_start = metadata_strip.frame_start
new_strip.channel = channel
new_strip.name = f"{self.get_shot_name(metadata_strip)}{self.file_type.lower()}"
new_strip.colorspace_settings.name = new_strip.colorspace_settings.name
def get_shot_seq_directory(self, context, filepath):
addon_prefs = prefs.addon_prefs_get(context)
path_string = os.path.realpath(bpy.path.abspath(filepath))
path = Path(
path_string.replace(addon_prefs.shot_playblast_root_dir, addon_prefs.frames_root_dir)
)
return path.parent
def execute(self, context: bpy.types.Context) -> Set[str]:
# Get closest empty channel
succeeded = []
failed = []
channel = int(self.channel_selection)
addon_prefs = prefs.addon_prefs_get(context)
if not (Path(addon_prefs.frames_root_dir).is_dir() and addon_prefs.frames_root_dir != ''):
self.report({"ERROR"}, f"Frames Directory does not exist, check add-on preferences")
return {"CANCELLED"}
if self.set_color_space:
self.set_scene_colorspace(context)
metadata_strips = [
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():
failed.append(str(directory))
continue
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"}
class KITSU_OT_sqe_clear_update_indicators(bpy.types.Operator):
bl_idname = "kitsu.sqe_clear_update_indicators"
bl_label = "Clear Update Indicators"
bl_description = "Removes the media update indicators from all strips"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
if not sqe:
return False
return bool(sqe.sequences_all)
def execute(self, context: bpy.types.Context) -> Set[str]:
addon_prefs = prefs.addon_prefs_get(context)
reset: List[bpy.types.Strip] = []
sequences = context.selected_sequences
if not sequences:
sequences = context.scene.sequence_editor.sequences_all
for strip in sequences:
if strip.kitsu.media_outdated:
strip.kitsu.media_outdated = False
reset.append(strip)
if not reset:
self.report({"INFO"}, "Already reset")
return {"FINISHED"}
self.report(
{"INFO"},
f"Cleared indicator of {len(reset)} {'strip' if len(reset) == 1 else 'strips'}",
)
util.ui_redraw()
return {"FINISHED"}
class KITSU_OT_sqe_change_strip_source(bpy.types.Operator):
bl_idname = "kitsu.sqe_change_strip_source"
bl_label = "Change Strip Media Source"
bl_description = (
"Changes the media source of the active strip by "
"cycling through the different versions on disk"
)
bl_options = {"REGISTER", "UNDO"}
direction: bpy.props.EnumProperty(items=[("UP", "UP", ""), ("DOWN", "DOWN", "")])
go_latest: bpy.props.BoolProperty(name="Got to latest", default=False)
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
sqe = context.scene.sequence_editor
if not sqe:
return False
return bool(sqe.active_strip)
def execute(self, context: bpy.types.Context) -> Set[str]:
strip = context.scene.sequence_editor.active_strip
# Check if it has valid filepath key.
if not strip.filepath:
self.report({"ERROR"}, f"{strip.name} has invalid filepath")
return {"CANCELLED"}
media_path_old = Path(os.path.abspath(bpy.path.abspath(strip.filepath)))
current_version = util.get_version(media_path_old.name)
# Check if source media path contains version string.
if not current_version:
self.report(
{"ERROR"},
f"{strip.name} source media contains no version string: {strip.filepath}",
)
return {"CANCELLED"}
# Gather valid files to compare source media to.
media_folder = media_path_old.parent
media_all: List[Path] = [
f for f in media_folder.iterdir() if f.is_file() and util.get_version(f.name)
]
# List of files that are named as source except for version str.
valid_files: List[Path] = []
for file in media_all:
# Version should exists here, we already check for that in list comprehension.
version = util.get_version(file.name)
# We only want to consider files that have the same name except for version string.
if file.name.replace(version, "") != media_path_old.name.replace(current_version, ""):
continue
valid_files.append(file)
valid_files.sort(reverse=True)
# No valid files found, should not happen source file should be at least here.
if not valid_files:
self.report({"WARNING"}, f"{strip.name} no other files available")
current_idx = valid_files.index(media_path_old)
if self.go_latest:
latest_index = 0
# Check if already on latest version.
if current_idx == latest_index:
self.report(
{"INFO"},
f"Already at latest version: {util.get_version(valid_files[0].name)}",
)
else:
self.report(
{"INFO"},
f"Reached latest version: {util.get_version(valid_files[0].name)}",
)
strip.filepath = bpy.path.relpath(valid_files[latest_index].as_posix())
strip.kitsu.media_outdated = False
# Needs to be reset otherwise other operator instances also do go_latest.
self.go_latest = False
elif self.direction == "UP":
new_index = current_idx - 1
if new_index <= 0:
self.report(
{"INFO"},
f"Reached latest version: {util.get_version(valid_files[0].name)}",
)
strip.kitsu.media_outdated = False
if current_idx == 0:
return {"FINISHED"}
if new_index >= 0:
strip.filepath = bpy.path.relpath(valid_files[new_index].as_posix())
elif self.direction == "DOWN":
new_index = current_idx + 1
if new_index >= len(valid_files) - 1:
self.report(
{"INFO"},
f"Reached oldest version: {util.get_version(valid_files[-1].name)}",
)
if current_idx == len(valid_files) - 1:
return {"FINISHED"}
if new_index <= len(valid_files) - 1:
strip.filepath = bpy.path.relpath(valid_files[new_index].as_posix())
strip.kitsu.media_outdated = True
# Load latest media.
logger.info(
"%s changed source media version: %s > %s",
strip.name,
current_version,
util.get_version(Path(strip.filepath).name),
)
util.ui_redraw()
return {"FINISHED"}
# ---------REGISTER ----------.
classes = [
KITSU_OT_sqe_push_new_sequence,
KITSU_OT_sqe_push_new_shot,
KITSU_OT_sqe_push_shot_meta,
KITSU_OT_sqe_uninit_strip,
KITSU_OT_sqe_unlink_shot,
KITSU_OT_sqe_init_strip,
KITSU_OT_sqe_link_shot,
KITSU_OT_sqe_link_sequence,
KITSU_OT_sqe_set_thumbnail_task_type,
KITSU_OT_sqe_set_sqe_render_task_type,
KITSU_OT_sqe_push_render_still,
KITSU_OT_sqe_push_render,
KITSU_OT_sqe_push_shot,
KITSU_OT_sqe_push_del_shot,
KITSU_OT_sqe_pull_shot_meta,
KITSU_OT_sqe_multi_edit_strip,
KITSU_OT_sqe_debug_duplicates,
KITSU_OT_sqe_debug_not_linked,
KITSU_OT_sqe_debug_multi_project,
KITSU_OT_sqe_pull_edit,
KITSU_OT_sqe_init_strip_start_frame,
KITSU_OT_sqe_create_metadata_strip,
KITSU_OT_sqe_fix_metadata_strips,
KITSU_OT_sqe_add_sequence_color,
KITSU_OT_sqe_scan_for_media_updates,
KITSU_OT_sqe_change_strip_source,
KITSU_OT_sqe_clear_update_indicators,
KITSU_OT_sqe_import_image_sequence,
KITSU_OT_sqe_import_playblast,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)