diff options
author | Nathan Lovato <nathan@gdquest.com> | 2021-01-24 03:17:35 +0300 |
---|---|---|
committer | Nathan Lovato <nathan@gdquest.com> | 2021-01-24 03:17:35 +0300 |
commit | cd176b2617bd3ede969c3aa218ee54a79fc69f27 (patch) | |
tree | b4f51b2c46a95f1ebb8a001269cd8ae6f417bf2a /power_sequencer | |
parent | b482ca0078d5640306985f478bb1dec3dbe7219b (diff) |
Update power sequencer to v2.0.1
Changelog: https://github.com/GDQuest/blender-power-sequencer/blob/master/CHANGELOG.md#power-sequencer-201
Commit range: https://github.com/GDQuest/blender-power-sequencer/compare/1.5.0...2.0.1
Diffstat (limited to 'power_sequencer')
37 files changed, 276 insertions, 2331 deletions
diff --git a/power_sequencer/__init__.py b/power_sequencer/__init__.py index 3cb5d4f2..8d2c84db 100755 --- a/power_sequencer/__init__.py +++ b/power_sequencer/__init__.py @@ -38,11 +38,11 @@ bl_info = { "name": "Power Sequencer", "description": "Video editing tools for content creators", "author": "Nathan Lovato", - "version": (1, 5, 1), - "blender": (2, 90, 1), + "version": (1, 5, 0), + "blender": (2, 81, 0), "location": "Sequencer", "tracker_url": "https://github.com/GDquest/Blender-power-sequencer/issues", - "doc_url": "https://www.gdquest.com/docs/documentation/power-sequencer/", + "wiki_url": "https://www.gdquest.com/docs/documentation/power-sequencer/", "support": "COMMUNITY", "category": "Sequencer", } @@ -80,7 +80,7 @@ def register(): keymaps = register_shortcuts(classes_operator) addon_keymaps += keymaps - # print("Registered {} with {} modules".format(bl_info["name"], len(modules))) + print("Registered {} with {} modules".format(bl_info["name"], len(modules))) def unregister(): @@ -104,4 +104,4 @@ def unregister(): unregister_properties() unregister_handlers() - # print("Unregistered {}".format(bl_info["name"])) + print("Unregistered {}".format(bl_info["name"])) diff --git a/power_sequencer/addon_preferences.py b/power_sequencer/addon_preferences.py index 0ca31330..014f63dd 100644 --- a/power_sequencer/addon_preferences.py +++ b/power_sequencer/addon_preferences.py @@ -55,7 +55,7 @@ class PowerSequencerPreferences(bpy.types.AddonPreferences): error_message, info = "", "" try: info: str = subprocess.check_output([path, "-version"]).decode("utf-8") - info = info[:info.find("Copyright")] + info = info[: info.find("Copyright")] print(info) except (OSError, subprocess.CalledProcessError): error_message = "Path `{}` is not a valid ffmpeg executable".format(path) diff --git a/power_sequencer/operators/__init__.py b/power_sequencer/operators/__init__.py index 064ba9b3..406c8635 100755 --- a/power_sequencer/operators/__init__.py +++ b/power_sequencer/operators/__init__.py @@ -22,17 +22,13 @@ def get_operator_classes(): """Returns the list of operators in the add-on""" this_file = os.path.dirname(__file__) module_files = [ - f - for f in os.listdir(this_file) - if f.endswith(".py") and not f.startswith("__init__") + f for f in os.listdir(this_file) if f.endswith(".py") and not f.startswith("__init__") ] module_paths = ["." + os.path.splitext(f)[0] for f in module_files] classes = [] for path in module_paths: module = importlib.import_module(path, package="power_sequencer.operators") - operator_names = [ - entry for entry in dir(module) if entry.startswith("POWER_SEQUENCER_OT") - ] + operator_names = [entry for entry in dir(module) if entry.startswith("POWER_SEQUENCER_OT")] classes.extend([getattr(module, name) for name in operator_names]) return classes @@ -41,9 +37,7 @@ doc = { "sequencer.refresh_all": { "name": "Refresh All", "description": "", - "shortcuts": [ - ({"type": "R", "value": "PRESS", "shift": True}, {}, "Refresh All") - ], + "shortcuts": [({"type": "R", "value": "PRESS", "shift": True}, {}, "Refresh All")], "demo": "", "keymap": "Sequencer", } diff --git a/power_sequencer/operators/channel_offset.py b/power_sequencer/operators/channel_offset.py index 35da1b47..48305c4b 100644 --- a/power_sequencer/operators/channel_offset.py +++ b/power_sequencer/operators/channel_offset.py @@ -14,17 +14,12 @@ # You should have received a copy of the GNU General Public License along with Power Sequencer. If # not, see <https://www.gnu.org/licenses/>. # -import bpy from operator import attrgetter -from .utils.doc import doc_name, doc_idname, doc_brief, doc_description -from .utils.functions import ( - slice_selection, - get_frame_range, - get_channel_range, - trim_strips, - find_strips_in_range, -) +import bpy + +from .utils.doc import doc_brief, doc_description, doc_idname, doc_name +from .utils.functions import find_strips_in_range, move_selection, trim_strips class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator): @@ -39,7 +34,7 @@ class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator): "shortcuts": [ ( {"type": "UP_ARROW", "value": "PRESS", "alt": True}, - {"direction": "up"}, + {"direction": "up", "trim_target_channel": False}, "Move to Open Channel Above", ), ( @@ -49,7 +44,7 @@ class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator): ), ( {"type": "DOWN_ARROW", "value": "PRESS", "alt": True}, - {"direction": "down"}, + {"direction": "down", "trim_target_channel": False}, "Move to Open Channel Below", ), ( @@ -79,35 +74,71 @@ class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator): description="Trim strips to make space in the target channel", default=False, ) + keep_selection_offset: bpy.props.BoolProperty( + name="Keep selection offset", + description="The selected strips preserve their relative positions", + default=True, + ) @classmethod def poll(cls, context): return context.selected_sequences def execute(self, context): + + max_channel = 32 + min_channel = 1 + + if self.direction == "up": + channel_offset = +1 + limit_channel = max_channel + comparison_function = min + + if self.direction == "down": + channel_offset = -1 + limit_channel = min_channel + comparison_function = max + selection = [s for s in context.selected_sequences if not s.lock] + if not selection: return {"FINISHED"} - selection_blocks = slice_selection(context, selection) - for block in selection_blocks: - sequences = sorted(block, key=attrgetter("channel", "frame_final_start")) - frame_start, frame_end = get_frame_range(sequences) - channel_start, channel_end = get_channel_range(sequences) - - if self.trim_target_channel: - to_delete, to_trim = find_strips_in_range(frame_start, frame_end, context.sequences) - channel_trim = ( - channel_end + 1 if self.direction == "up" else max(1, channel_start - 1) - ) - to_trim = [s for s in to_trim if s.channel == channel_trim] - to_delete = [s for s in to_delete if s.channel == channel_trim] - trim_strips(context, frame_start, frame_end, to_trim, to_delete) - - if self.direction == "up": - for s in reversed(sequences): - s.channel += 1 - elif self.direction == "down": - for s in sequences: - s.channel = max(1, s.channel - 1) + sequences = sorted(selection, key=attrgetter("channel", "frame_final_start")) + if self.direction == "up": + sequences = [s for s in reversed(sequences)] + + head = sequences[0] + if not self.keep_selection_offset or ( + head.channel != limit_channel and self.keep_selection_offset + ): + for s in sequences: + if self.trim_target_channel: + channel_trim = s.channel + channel_offset + strips_in_trim_channel = [ + sequence + for sequence in context.sequences + if (sequence.channel == channel_trim) + ] + if strips_in_trim_channel: + to_delete, to_trim = find_strips_in_range( + s.frame_final_start, s.frame_final_end, strips_in_trim_channel + ) + trim_strips( + context, s.frame_final_start, s.frame_final_end, to_trim, to_delete + ) + + if not self.keep_selection_offset: + s.channel = comparison_function(limit_channel, s.channel + channel_offset) + if s.channel == limit_channel: + move_selection(context, [s], 0, 0) + + if self.keep_selection_offset: + start_frame = head.frame_final_start + x_difference = 0 + while not head.channel == limit_channel: + move_selection(context, sequences, -x_difference, channel_offset) + x_difference = head.frame_final_start - start_frame + if x_difference == 0: + break return {"FINISHED"} diff --git a/power_sequencer/operators/cut_strips_under_cursor.py b/power_sequencer/operators/cut_strips_under_cursor.py index d7e1141a..3b54a17b 100644 --- a/power_sequencer/operators/cut_strips_under_cursor.py +++ b/power_sequencer/operators/cut_strips_under_cursor.py @@ -30,9 +30,7 @@ class POWER_SEQUENCER_OT_split_strips_under_cursor(bpy.types.Operator): "name": doc_name(__qualname__), "demo": "https://i.imgur.com/ZyEd0jD.gif", "description": doc_description(__doc__), - "shortcuts": [ - ({"type": "K", "value": "PRESS"}, {}, "Cut All Strips Under Cursor") - ], + "shortcuts": [({"type": "K", "value": "PRESS"}, {}, "Cut All Strips Under Cursor")], "keymap": "Sequencer", } bl_idname = doc_idname(__qualname__) @@ -66,10 +64,5 @@ class POWER_SEQUENCER_OT_split_strips_under_cursor(bpy.types.Operator): deselect = False if deselect: bpy.ops.sequencer.select_all(action="DESELECT") - ( - context.selected_sequences - or bpy.ops.power_sequencer.select_strips_under_cursor() - ) - return bpy.ops.sequencer.split( - frame=context.scene.frame_current, side=self.side - ) + (context.selected_sequences or bpy.ops.power_sequencer.select_strips_under_cursor()) + return bpy.ops.sequencer.split(frame=context.scene.frame_current, side=self.side) diff --git a/power_sequencer/operators/delete_direct.py b/power_sequencer/operators/delete_direct.py index b39ae12f..4c6e4d2d 100644 --- a/power_sequencer/operators/delete_direct.py +++ b/power_sequencer/operators/delete_direct.py @@ -50,9 +50,7 @@ class POWER_SEQUENCER_OT_delete_direct(bpy.types.Operator): bl_description = doc_brief(doc["description"]) bl_options = {"REGISTER", "UNDO"} - is_removing_transitions: bpy.props.BoolProperty( - name="Remove Transitions", default=False - ) + is_removing_transitions: bpy.props.BoolProperty(name="Remove Transitions", default=False) @classmethod def poll(cls, context): @@ -61,17 +59,12 @@ class POWER_SEQUENCER_OT_delete_direct(bpy.types.Operator): def invoke(self, context, event): frame, channel = get_mouse_frame_and_channel(context, event) if not context.selected_sequences: - bpy.ops.power_sequencer.select_closest_to_mouse( - frame=frame, channel=channel - ) + bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel) return self.execute(context) def execute(self, context): selection = context.selected_sequences - if ( - self.is_removing_transitions - and bpy.ops.power_sequencer.transitions_remove.poll() - ): + if self.is_removing_transitions and bpy.ops.power_sequencer.transitions_remove.poll(): bpy.ops.power_sequencer.transitions_remove() bpy.ops.sequencer.delete() diff --git a/power_sequencer/operators/expand_to_surrounding_cuts.py b/power_sequencer/operators/expand_to_surrounding_cuts.py index 1435b23c..53de8bf7 100644 --- a/power_sequencer/operators/expand_to_surrounding_cuts.py +++ b/power_sequencer/operators/expand_to_surrounding_cuts.py @@ -33,11 +33,7 @@ class POWER_SEQUENCER_OT_expand_to_surrounding_cuts(bpy.types.Operator): "demo": "", "description": doc_description(__doc__), "shortcuts": [ - ( - {"type": "E", "value": "PRESS", "ctrl": True}, - {}, - "Expand to Surrounding Cuts", - ) + ({"type": "E", "value": "PRESS", "ctrl": True}, {}, "Expand to Surrounding Cuts",) ], "keymap": "Sequencer", } @@ -68,37 +64,24 @@ class POWER_SEQUENCER_OT_expand_to_surrounding_cuts(bpy.types.Operator): sequences_frame_start = min( sequences, key=lambda s: s.frame_final_start ).frame_final_start - sequences_frame_end = max( - sequences, key=lambda s: s.frame_final_end - ).frame_final_end + sequences_frame_end = max(sequences, key=lambda s: s.frame_final_end).frame_final_end frame_left, frame_right = find_closest_cuts( context, sequences_frame_start, sequences_frame_end ) - if ( - sequences_frame_start == frame_left - and sequences_frame_end == frame_right - ): + if sequences_frame_start == frame_left and sequences_frame_end == frame_right: continue - to_extend_left = [ - s for s in sequences if s.frame_final_start == sequences_frame_start - ] - to_extend_right = [ - s for s in sequences if s.frame_final_end == sequences_frame_end - ] + to_extend_left = [s for s in sequences if s.frame_final_start == sequences_frame_start] + to_extend_right = [s for s in sequences if s.frame_final_end == sequences_frame_end] for s in to_extend_left: s.frame_final_start = ( - frame_left - if frame_left < sequences_frame_start - else sequences_frame_start + frame_left if frame_left < sequences_frame_start else sequences_frame_start ) for s in to_extend_right: s.frame_final_end = ( - frame_right - if frame_right > sequences_frame_end - else sequences_frame_end + frame_right if frame_right > sequences_frame_end else sequences_frame_end ) return {"FINISHED"} @@ -110,8 +93,6 @@ def find_closest_cuts(context, frame_min, frame_max): ).frame_final_end frame_right = min( context.sequences, - key=lambda s: s.frame_final_start - if s.frame_final_start >= frame_max - else 1000000, + key=lambda s: s.frame_final_start if s.frame_final_start >= frame_max else 1000000, ).frame_final_start return frame_left, frame_right diff --git a/power_sequencer/operators/fade_add.py b/power_sequencer/operators/fade_add.py index 1f4d0bfa..dd75c864 100644 --- a/power_sequencer/operators/fade_add.py +++ b/power_sequencer/operators/fade_add.py @@ -51,10 +51,7 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} duration_seconds: bpy.props.FloatProperty( - name="Fade Duration", - description="Duration of the fade in seconds", - default=1.0, - min=0.01, + name="Fade Duration", description="Duration of the fade in seconds", default=1.0, min=0.01, ) type: bpy.props.EnumProperty( items=[ @@ -98,12 +95,8 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator): if s.frame_final_start < context.scene.frame_current < s.frame_final_end ] - max_duration = min( - sequences, key=lambda s: s.frame_final_duration - ).frame_final_duration - max_duration = ( - floor(max_duration / 2.0) if self.type == "IN_OUT" else max_duration - ) + max_duration = min(sequences, key=lambda s: s.frame_final_duration).frame_final_duration + max_duration = floor(max_duration / 2.0) if self.type == "IN_OUT" else max_duration faded_sequences = [] for sequence in sequences: @@ -113,15 +106,9 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator): if not self.is_long_enough(sequence, duration): continue - animated_property = ( - "volume" if hasattr(sequence, "volume") else "blend_alpha" - ) - fade_fcurve = fade_find_or_create_fcurve( - context, sequence, animated_property - ) - fades = self.calculate_fades( - sequence, fade_fcurve, animated_property, duration - ) + animated_property = "volume" if hasattr(sequence, "volume") else "blend_alpha" + fade_fcurve = fade_find_or_create_fcurve(context, sequence, animated_property) + fades = self.calculate_fades(sequence, fade_fcurve, animated_property, duration) fade_animation_clear(context, fade_fcurve, fades) fade_animation_create(fade_fcurve, fades) faded_sequences.append(sequence) @@ -129,9 +116,7 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator): sequence_string = "sequence" if len(faded_sequences) == 1 else "sequences" self.report( {"INFO"}, - "Added fade animation to {} {}.".format( - len(faded_sequences), sequence_string - ), + "Added fade animation to {} {}.".format(len(faded_sequences), sequence_string), ) return {"FINISHED"} @@ -232,13 +217,9 @@ class Fade: if type == "IN": self.start = Vector((sequence.frame_final_start, 0.0)) - self.end = Vector( - (sequence.frame_final_start + self.duration, self.max_value) - ) + self.end = Vector((sequence.frame_final_start + self.duration, self.max_value)) elif type == "OUT": - self.start = Vector( - (sequence.frame_final_end - self.duration, self.max_value) - ) + self.start = Vector((sequence.frame_final_end - self.duration, self.max_value)) self.end = Vector((sequence.frame_final_end, 0.0)) def calculate_max_value(self, sequence, fade_fcurve): @@ -253,15 +234,11 @@ class Fade: else: if self.type == "IN": fade_end = sequence.frame_final_start + self.duration - keyframes = ( - k for k in fade_fcurve.keyframe_points if k.co[0] >= fade_end - ) + keyframes = (k for k in fade_fcurve.keyframe_points if k.co[0] >= fade_end) if self.type == "OUT": fade_start = sequence.frame_final_end - self.duration keyframes = ( - k - for k in reversed(fade_fcurve.keyframe_points) - if k.co[0] <= fade_start + k for k in reversed(fade_fcurve.keyframe_points) if k.co[0] <= fade_start ) try: max_value = next(keyframes).co[1] @@ -275,6 +252,4 @@ class Fade: def calculate_duration_frames(context, duration_seconds): - return round( - duration_seconds * context.scene.render.fps / context.scene.render.fps_base - ) + return round(duration_seconds * context.scene.render.fps / context.scene.render.fps_base) diff --git a/power_sequencer/operators/fade_clear.py b/power_sequencer/operators/fade_clear.py index 06b320b0..02516633 100644 --- a/power_sequencer/operators/fade_clear.py +++ b/power_sequencer/operators/fade_clear.py @@ -32,11 +32,7 @@ class POWER_SEQUENCER_OT_fade_clear(bpy.types.Operator): "demo": "", "description": doc_description(__doc__), "shortcuts": [ - ( - {"type": "F", "value": "PRESS", "alt": True, "ctrl": True}, - {}, - "Clear Fades", - ) + ({"type": "F", "value": "PRESS", "alt": True, "ctrl": True}, {}, "Clear Fades",) ], "keymap": "Sequencer", } @@ -53,9 +49,7 @@ class POWER_SEQUENCER_OT_fade_clear(bpy.types.Operator): fcurves = context.scene.animation_data.action.fcurves for sequence in context.selected_sequences: - animated_property = ( - "volume" if hasattr(sequence, "volume") else "blend_alpha" - ) + animated_property = "volume" if hasattr(sequence, "volume") else "blend_alpha" data_path = sequence.path_from_id() + "." + animated_property fcurve_map = { curve.data_path: curve diff --git a/power_sequencer/operators/gap_remove.py b/power_sequencer/operators/gap_remove.py index 4a736051..825bfc0d 100644 --- a/power_sequencer/operators/gap_remove.py +++ b/power_sequencer/operators/gap_remove.py @@ -71,9 +71,7 @@ class POWER_SEQUENCER_OT_gap_remove(bpy.types.Operator): else context.sequences ) sequences = [ - s - for s in sequences - if s.frame_final_start >= frame or s.frame_final_end > frame + s for s in sequences if s.frame_final_start >= frame or s.frame_final_end > frame ] sequence_blocks = slice_selection(context, sequences) if not sequence_blocks: @@ -100,18 +98,12 @@ class POWER_SEQUENCER_OT_gap_remove(bpy.types.Operator): Finds and returns the frame at which the gap starts. Takes a list sequences sorted by frame_final_start. """ - strips_start = min( - sorted_sequences, key=attrgetter("frame_final_start") - ).frame_final_start - strips_end = max( - sorted_sequences, key=attrgetter("frame_final_end") - ).frame_final_end + strips_start = min(sorted_sequences, key=attrgetter("frame_final_start")).frame_final_start + strips_end = max(sorted_sequences, key=attrgetter("frame_final_end")).frame_final_end gap_frame = -1 if strips_start > frame: - strips_before_frame_start = [ - s for s in context.sequences if s.frame_final_end <= frame - ] + strips_before_frame_start = [s for s in context.sequences if s.frame_final_end <= frame] frame_target = 0 if strips_before_frame_start: frame_target = max( diff --git a/power_sequencer/operators/make_hold_frame.py b/power_sequencer/operators/make_hold_frame.py index c6893d04..22c2eecf 100644 --- a/power_sequencer/operators/make_hold_frame.py +++ b/power_sequencer/operators/make_hold_frame.py @@ -77,9 +77,7 @@ class POWER_SEQUENCER_OT_make_hold_frame(bpy.types.Operator): try: next_strip_start = next( s - for s in sorted( - context.sequences, key=operator.attrgetter("frame_final_start") - ) + for s in sorted(context.sequences, key=operator.attrgetter("frame_final_start")) if s.frame_final_start > active.frame_final_end ).frame_final_start offset = next_strip_start - active.frame_final_end @@ -90,9 +88,7 @@ class POWER_SEQUENCER_OT_make_hold_frame(bpy.types.Operator): source_blend_type = active.blend_type sequencer.split(frame=scene.frame_current, type="SOFT", side="RIGHT") transform.seq_slide(value=(offset, 0)) - sequencer.split( - frame=scene.frame_current + offset + 1, type="SOFT", side="LEFT" - ) + sequencer.split(frame=scene.frame_current + offset + 1, type="SOFT", side="LEFT") transform.seq_slide(value=(-offset, 0)) sequencer.meta_make() diff --git a/power_sequencer/operators/scene_create_from_selection.py b/power_sequencer/operators/scene_create_from_selection.py index b615dae9..cd774d0d 100644 --- a/power_sequencer/operators/scene_create_from_selection.py +++ b/power_sequencer/operators/scene_create_from_selection.py @@ -67,7 +67,6 @@ class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator): context.window.scene.name = context.selected_sequences[0].name new_scene_name = context.window.scene.name - ###after full copy also unselected strips are in the sequencer... Delete those strips bpy.ops.sequencer.select_all(action="INVERT") bpy.ops.power_sequencer.delete_direct() @@ -85,8 +84,10 @@ class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator): bpy.ops.power_sequencer.delete_direct() bpy.ops.sequencer.scene_strip_add( - frame_start=selection_start_frame, channel=selection_start_channel, scene=new_scene_name + frame_start=selection_start_frame, + channel=selection_start_channel, + scene=new_scene_name, ) scene_strip = context.selected_sequences[0] - # scene_strip.use_sequence = True + # scene_strip.use_sequence = True return {"FINISHED"} diff --git a/power_sequencer/operators/scene_merge_from.py b/power_sequencer/operators/scene_merge_from.py index 16b2710d..5c3f40b0 100644 --- a/power_sequencer/operators/scene_merge_from.py +++ b/power_sequencer/operators/scene_merge_from.py @@ -73,9 +73,7 @@ class POWER_SEQUENCER_OT_merge_from_scene_strip(bpy.types.Operator): context.window.scene = strip_scene bpy.ops.scene.delete() context.window.scene = start_scene - self.report( - type={"WARNING"}, message="Merged scenes lose all their animation data." - ) + self.report(type={"WARNING"}, message="Merged scenes lose all their animation data.") return {"FINISHED"} diff --git a/power_sequencer/operators/select_all_left_or_right.py b/power_sequencer/operators/select_all_left_or_right.py index b2e08bb8..01e35306 100644 --- a/power_sequencer/operators/select_all_left_or_right.py +++ b/power_sequencer/operators/select_all_left_or_right.py @@ -62,4 +62,4 @@ class POWER_SEQUENCER_OT_select_all_left_or_right(bpy.types.Operator): return context.sequences def execute(self, context): - return bpy.ops.sequencer.select_side_of_frame("INVOKE_DEFAULT", side=self.side) + return bpy.ops.sequencer.select("INVOKE_DEFAULT", left_right=self.side) diff --git a/power_sequencer/operators/snap.py b/power_sequencer/operators/snap.py index 42b029d1..13d5e66e 100644 --- a/power_sequencer/operators/snap.py +++ b/power_sequencer/operators/snap.py @@ -32,11 +32,7 @@ class POWER_SEQUENCER_OT_snap(bpy.types.Operator): "demo": "", "description": doc_description(__doc__), "shortcuts": [ - ( - {"type": "S", "value": "PRESS", "shift": True}, - {}, - "Snap sequences to cursor", - ) + ({"type": "S", "value": "PRESS", "shift": True}, {}, "Snap sequences to cursor",) ], "keymap": "Sequencer", } diff --git a/power_sequencer/operators/snap_selection.py b/power_sequencer/operators/snap_selection.py index 5900579b..eb2851b5 100644 --- a/power_sequencer/operators/snap_selection.py +++ b/power_sequencer/operators/snap_selection.py @@ -15,7 +15,7 @@ # not, see <https://www.gnu.org/licenses/>. # import bpy -from .utils.functions import get_sequences_under_cursor, apply_time_offset +from .utils.functions import get_sequences_under_cursor, move_selection from .utils.doc import doc_name, doc_idname, doc_brief, doc_description @@ -32,11 +32,7 @@ class POWER_SEQUENCER_OT_snap_selection(bpy.types.Operator): "demo": "", "description": doc_description(__doc__), "shortcuts": [ - ( - {"type": "S", "value": "PRESS", "alt": True}, - {}, - "Snap selection to cursor", - ) + ({"type": "S", "value": "PRESS", "alt": True}, {}, "Snap selection to cursor",) ], "keymap": "Sequencer", } @@ -55,9 +51,7 @@ class POWER_SEQUENCER_OT_snap_selection(bpy.types.Operator): if context.selected_sequences else get_sequences_under_cursor(context) ) - frame_first = min( - sequences, key=lambda s: s.frame_final_start - ).frame_final_start + frame_first = min(sequences, key=lambda s: s.frame_final_start).frame_final_start time_offset = context.scene.frame_current - frame_first - apply_time_offset(context, sequences, time_offset) + move_selection(context, sequences, time_offset) return {"FINISHED"} diff --git a/power_sequencer/operators/trim_left_or_right_handles.py b/power_sequencer/operators/trim_left_or_right_handles.py index b93bee53..bbbb8bd5 100644 --- a/power_sequencer/operators/trim_left_or_right_handles.py +++ b/power_sequencer/operators/trim_left_or_right_handles.py @@ -78,16 +78,9 @@ class POWER_SEQUENCER_OT_trim_left_or_right_handles(bpy.types.Operator): frame_current = context.scene.frame_current # Only select sequences under the time cursor - sequences = ( - context.selected_sequences - if context.selected_sequences - else context.sequences - ) + sequences = context.selected_sequences if context.selected_sequences else context.sequences for s in sequences: - s.select = ( - s.frame_final_start <= frame_current - and s.frame_final_end >= frame_current - ) + s.select = s.frame_final_start <= frame_current and s.frame_final_end >= frame_current sequences = [s for s in sequences if s.select] if not sequences: return {"FINISHED"} diff --git a/power_sequencer/operators/utils/functions.py b/power_sequencer/operators/utils/functions.py index 3462b767..5b33067c 100644 --- a/power_sequencer/operators/utils/functions.py +++ b/power_sequencer/operators/utils/functions.py @@ -21,6 +21,9 @@ import bpy from .global_settings import SequenceTypes +max_channel = 32 +min_channel = 1 + def calculate_distance(x1, y1, x2, y2): return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) @@ -34,10 +37,8 @@ def find_linked(context, sequences, selected_sequences): """ Takes a list of sequences and returns a list of all the sequences and effects that are linked in time - Args: - - sequences: a list of sequences - + - sequences: a list of sequences Returns a list of all the linked sequences, but not the sequences passed to the function """ start, end = get_frame_range(sequences, selected_sequences) @@ -99,7 +100,7 @@ def find_sequences_after(context, sequence): """ Finds the strips following the sequences passed to the function Args: - - Sequences, the sequences to check + - Sequences, the sequences to check Returns all the strips after the sequence in the current context """ return [s for s in context.sequences if s.frame_final_start > sequence.frame_final_start] @@ -133,12 +134,10 @@ def find_snap_candidate(context, frame=0): def find_strips_mouse(context, frame, channel, select_linked=False): """ Finds a list of sequences to select based on the frame and channel the mouse cursor is at - Args: - - frame: the frame the mouse or cursor is on - - channel: the channel the mouse is hovering - - select_linked: find and append the sequences linked in time if True - + - frame: the frame the mouse or cursor is on + - channel: the channel the mouse is hovering + - select_linked: find and append the sequences linked in time if True Returns the sequence(s) under the mouse cursor as a list Returns an empty list if nothing found """ @@ -201,10 +200,9 @@ def get_mouse_frame_and_channel(context, event): def is_in_range(context, sequence, start, end): """ Checks if a single sequence's start or end is in the range - Args: - - sequence: the sequence to check for - - start, end: the start and end frames + - sequence: the sequence to check for + - start, end: the start and end frames Returns True if the sequence is within the range, False otherwise """ s_start = sequence.frame_final_start @@ -225,47 +223,32 @@ def set_preview_range(context, start, end): scene.frame_preview_end = end -def slice_selection(context, sequences): +def slice_selection(context, sequences, range_block=0): """ Takes a list of sequences and breaks it down into multiple lists of connected sequences - Returns a list of lists of sequences, each list corresponding to a block of sequences that are connected in time and sorted by frame_final_start """ - # Find when 2 sequences are not connected in time if not sequences: return [] - break_ids = [0] + # Indicates the index number of the lists from the "broken_selection" list + index = -1 + block_end = 0 + broken_selection = [] sorted_sequences = sorted(sequences, key=attrgetter("frame_final_start")) - last_sequence = sorted_sequences[0] - last_biggest_frame_end = last_sequence.frame_final_end - index = 0 + for s in sorted_sequences: - if s.frame_final_start > last_biggest_frame_end + 1: - break_ids.append(index) - last_biggest_frame_end = max(last_biggest_frame_end, s.frame_final_end) - last_sequence = s - index += 1 - - # Create lists - break_ids.append(len(sorted_sequences)) - cuts_count = len(break_ids) - 1 - broken_selection = [] - index = 0 - while index < cuts_count: - temp_list = [] - index_range = range(break_ids[index], break_ids[index + 1] - 1) - if len(index_range) == 0: - temp_list.append(sorted_sequences[break_ids[index]]) - else: - for counter in range(break_ids[index], break_ids[index + 1]): - temp_list.append(sorted_sequences[counter]) - if temp_list: - broken_selection.append(temp_list) - index += 1 + if not broken_selection or (block_end + 1 + range_block < s.frame_final_start): + broken_selection.append([s]) + block_end = s.frame_final_end + index += 1 + continue + block_end = max(block_end, s.frame_final_end) + broken_selection[index].append(s) + return broken_selection @@ -278,8 +261,10 @@ def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]): trim_end = max(frame_start, frame_end) to_trim = [s for s in to_trim if s.type in SequenceTypes.CUTABLE] + initial_selection = context.selected_sequences for s in to_trim: + strips_in_target_channel = [] # Cut strip longer than the trim range in three is_strip_longer_than_trim_range = ( s.frame_final_start < trim_start and s.frame_final_end > trim_end @@ -290,6 +275,13 @@ def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]): bpy.ops.sequencer.split(frame=trim_start, type="SOFT", side="RIGHT") bpy.ops.sequencer.split(frame=trim_end, type="SOFT", side="LEFT") to_delete.append(context.selected_sequences[0]) + + for c in context.sequences: + if c.channel == s.channel: + strips_in_target_channel.append(c) + + if s in initial_selection: + initial_selection.append(strips_in_target_channel[0]) continue # Resize strips that overlap the trim range @@ -298,20 +290,12 @@ def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]): elif s.frame_final_end > trim_start and s.frame_final_start < trim_start: s.frame_final_end = trim_start - delete_strips(to_delete) - return {"FINISHED"} - - -def delete_strips(to_delete): - """ - Deletes the list of sequences `to_delete` - """ - if not to_delete: - return - bpy.ops.sequencer.select_all(action="DESELECT") for s in to_delete: + bpy.context.sequences.remove(s) + + for s in initial_selection: s.select = True - bpy.ops.sequencer.delete() + return {"FINISHED"} def find_closest_surrounding_cuts(context, frame): @@ -378,30 +362,7 @@ def ripple_move(context, sequences, duration_frames, delete=False): else: to_ripple = set(to_ripple + sequences) - # Use the built-in seq_slide operator to move strips, for best performances - initial_selection = context.selected_sequences - bpy.ops.sequencer.select_all(action="DESELECT") - for s in to_ripple: - s.select = True - bpy.ops.transform.seq_slide(value=(duration_frames, 0)) - bpy.ops.sequencer.select_all(action="DESELECT") - for s in initial_selection: - s.select = True - - -def apply_time_offset(context, sequences=[], offset=0): - """Offsets a list of sequences in time using bpy.ops.transform.seq_slide. Mutates and restores the - user's selection. Use this function to ensure maximum performances and avoid having to figure - out the logic to move strips in the right order. - """ - selection = context.selected_sequences - bpy.ops.sequencer.select_all(action="DESELECT") - for s in sequences: - s.select = True - bpy.ops.transform.seq_slide(value=(offset, 0)) - bpy.ops.sequencer.select_all(action="DESELECT") - for s in selection: - s.select = True + move_selection(context, to_ripple, duration_frames, 0) def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=True): @@ -409,18 +370,18 @@ def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=Tru Returns a tuple of two lists: (strips_inside_range, strips_overlapping_range) strips_inside_range are strips entirely contained in the frame range. strips_overlapping_range are strips that only overlap the frame range. - Args: - - frame_start, the start of the frame range - - frame_end, the end of the frame range - - sequences (optional): only work with these sequences. - If it doesn't receive any, the function works with all the sequences in the current context - - find_overlapping (optional): find and return a list of strips that overlap the + - frame_start, the start of the frame range + - frame_end, the end of the frame range + - sequences (optional): only work with these sequences. + If it doesn't receive any, the function works with all the sequences in the current context + - find_overlapping (optional): find and return a list of strips that overlap the frame range - """ strips_inside_range = [] strips_overlapping_range = [] + if not sequences: + sequences = bpy.context.sequences for s in sequences: if ( frame_start <= s.frame_final_start <= frame_end @@ -429,8 +390,8 @@ def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=Tru strips_inside_range.append(s) elif find_overlapping: if ( - frame_start <= s.frame_final_end <= frame_end - or frame_start <= s.frame_final_start <= frame_end + frame_start < s.frame_final_end <= frame_end + or frame_start <= s.frame_final_start < frame_end ): strips_overlapping_range.append(s) @@ -438,3 +399,19 @@ def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=Tru if s.frame_final_start < frame_start and s.frame_final_end > frame_end: strips_overlapping_range.append(s) return strips_inside_range, strips_overlapping_range + + +def move_selection(context, sequences, frame_offset, channel_offset=0): + """Offsets the selected `sequences` horizontally and vertically and preserves + the current selected sequences. + """ + if not sequences: + return + initial_selection = context.selected_sequences + bpy.ops.sequencer.select_all(action="DESELECT") + for s in sequences: + s.select = True + bpy.ops.transform.seq_slide(value=(frame_offset, channel_offset)) + bpy.ops.sequencer.select_all(action="DESELECT") + for s in initial_selection: + s.select = True diff --git a/power_sequencer/operators/value_offset.py b/power_sequencer/operators/value_offset.py new file mode 100644 index 00000000..debd2ef6 --- /dev/null +++ b/power_sequencer/operators/value_offset.py @@ -0,0 +1,94 @@ +# +# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors +# +# This file is part of Power Sequencer. +# +# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the +# GNU General Public License as published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with Power Sequencer. If +# not, see <https://www.gnu.org/licenses/>. +# +import bpy + +from .utils.doc import doc_brief, doc_description, doc_idname, doc_name +from .utils.functions import convert_duration_to_frames + + +class POWER_SEQUENCER_OT_value_offset(bpy.types.Operator): + """Instantly offset selected strips, either using frames or seconds. Allows to + nudge the selection quickly, using keyboard shortcuts. + """ + + doc = { + "name": doc_name(__qualname__), + "demo": "", + "description": doc_description(__doc__), + "shortcuts": [ + ( + {"type": "LEFT_ARROW", "value": "PRESS", "shift": True, "alt": True}, + {"direction": "left"}, + "Offset the selection to the left.", + ), + ( + {"type": "RIGHT_ARROW", "value": "PRESS", "shift": True, "alt": True}, + {"direction": "right"}, + "Offset the selection to the right.", + ), + ], + "keymap": "Sequencer", + } + bl_idname = doc_idname(__qualname__) + bl_label = doc["name"] + bl_description = doc_brief(doc["description"]) + bl_options = {"REGISTER", "UNDO"} + + direction: bpy.props.EnumProperty( + items=[ + ("left", "left", "Move the selection to the left"), + ("right", "right", "Move the selection to the right"), + ], + name="Direction", + description="Move the selection given frames or seconds", + default="right", + options={"HIDDEN"}, + ) + value_type: bpy.props.EnumProperty( + items=[ + ("seconds", "Seconds", "Move with the value as seconds"), + ("frames", "Frames", "Move with the value as frames"), + ], + name="Value Type", + description="Toggle between offset in frames or seconds", + default="seconds", + ) + offset: bpy.props.FloatProperty( + name="Offset", + description="Offset amount to apply", + default=1.0, + step=5, + precision=3, + ) + + @classmethod + def poll(cls, context): + return context.selected_sequences + + def invoke(self, context, event): + self.offset = abs(self.offset) + if self.direction == "left": + self.offset *= -1.0 + return self.execute(context) + + def execute(self, context): + offset_frames = ( + convert_duration_to_frames(context, self.offset) + if self.value_type == "seconds" + else self.offset + ) + return bpy.ops.transform.seq_slide(value=(offset_frames, 0)) diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/__init__.py b/power_sequencer/scripts/BPSProxy/bpsproxy/__init__.py deleted file mode 100644 index f14cfb6a..00000000 --- a/power_sequencer/scripts/BPSProxy/bpsproxy/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/__main__.py b/power_sequencer/scripts/BPSProxy/bpsproxy/__main__.py deleted file mode 100644 index d8f25204..00000000 --- a/power_sequencer/scripts/BPSProxy/bpsproxy/__main__.py +++ /dev/null @@ -1,171 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -""" -Tool to render video proxies using FFMPEG -Offers mp4 and webm options -""" -import argparse as ap -import glob as g -import logging as lg -import os.path as osp -import sys -from itertools import compress, starmap, tee - -from .call import call, call_makedirs -from .commands import get_commands, get_commands_vi -from .config import CONFIG as C -from .config import LOGGER, LOGLEV -from .utils import checktools, printw, printd, prints, ToolError - - -def find_files( - directory=".", ignored_directory=C["proxy_directory"], extensions=C["extensions"]["all"] -): - """ - Find files to process. - - Parameters - ---------- - directory: str - Working directory. - ignored_directory: str - Don't check for files in this directory. By default `BL_proxy`. - extensions: set(str) - Set of file extensions for filtering the directory tree. - - Returns - ------- - out: list(str) - List of file paths to be processed. - """ - if not osp.isdir(directory): - raise ValueError(("The given path '{}' is not a valid directory.".format(directory))) - xs = g.iglob("{}/**".format(osp.abspath(directory)), recursive=True) - xs = filter(lambda x: osp.isfile(x), xs) - xs = filter(lambda x: ignored_directory not in osp.dirname(x), xs) - xs = [x for x in xs if osp.splitext(x)[1].lower() in extensions] - return xs - - -def parse_arguments(cfg): - """ - Uses `argparse` to parse the command line arguments. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - - Returns - ------- - out: Namespace - Command line arguments. - """ - p = ap.ArgumentParser(description="Create proxies for Blender VSE using FFMPEG.") - p.add_argument( - "working_directory", - nargs="?", - default=".", - help="The directory containing media to create proxies for", - ) - p.add_argument( - "-p", - "--preset", - default="mp4", - choices=cfg["presets"], - help="a preset name for proxy encoding", - ) - p.add_argument( - "-s", - "--sizes", - nargs="+", - type=int, - default=[25], - choices=cfg["proxy_sizes"], - help="A list of sizes of the proxies to render, either 25, 50, or 100", - ) - p.add_argument( - "-v", "--verbose", action="count", default=0, help="Increase verbosity level (eg. -vvv)." - ) - p.add_argument( - "--dry-run", - action="store_true", - help=( - "Run the script without actual rendering or creating files and" - " folders. For DEBUGGING purposes" - ), - ) - - clargs = p.parse_args() - # normalize directory - clargs.working_directory = osp.abspath(clargs.working_directory) - # --dry-run implies maximum verbosity level - clargs.verbose = 99999 if clargs.dry_run else clargs.verbose - return clargs - - -def main(): - """ - Script entry point. - """ - tools = ["ffmpeg", "ffprobe"] - try: - # get command line arguments and set log level - clargs = parse_arguments(C) - lg.basicConfig(level=LOGLEV[min(clargs.verbose, len(LOGLEV) - 1)]) - - # log basic command line arguments - clargs.dry_run and LOGGER.info("DRY-RUN") - LOGGER.info("WORKING-DIRECTORY :: {}".format(clargs.working_directory)) - LOGGER.info("PRESET :: {}".format(clargs.preset)) - LOGGER.info("SIZES :: {}".format(clargs.sizes)) - - # check for external dependencies - checktools(tools) - - # find files to process - path_i = find_files(clargs.working_directory) - kwargs = {"path_i": path_i} - - printw(C, "Creating directories if necessary") - call_makedirs(C, clargs, **kwargs) - - printw(C, "Checking for existing proxies") - cmds = tee(get_commands(C, clargs, what="check", **kwargs)) - stdouts = call(C, clargs, cmds=cmds[0], check=False, shell=True, **kwargs) - checks = map(lambda s: s.strip().split(), stdouts) - checks = starmap(lambda fst, *tail: not all(fst == t for t in tail), checks) - kwargs["path_i"] = list(compress(kwargs["path_i"], checks)) - - if len(kwargs["path_i"]) != 0: - printw(C, "Processing", s="\n") - cmds = get_commands_vi(C, clargs, **kwargs) - call(C, clargs, cmds=cmds, **kwargs) - else: - printd(C, "All proxies exist or no files found, nothing to process", s="\n") - printd(C, "Done") - except (ToolError, ValueError) as e: - LOGGER.error(e) - prints(C, "Exiting") - except KeyboardInterrupt: - prints(C, "DirtyInterrupt. Exiting", s="\n\n") - sys.exit() - - -# this is so it can be ran as a module: `python3 -m bpsrender` (for testing) -if __name__ == "__main__": - main() diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/call.py b/power_sequencer/scripts/BPSProxy/bpsproxy/call.py deleted file mode 100644 index dd74e3a8..00000000 --- a/power_sequencer/scripts/BPSProxy/bpsproxy/call.py +++ /dev/null @@ -1,95 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -# import multiprocessing as mp -import os -import subprocess as sp -import sys - -from functools import partial -from itertools import chain, tee -from tqdm import tqdm -from .config import LOGGER -from .utils import get_dir, kickstart - -WINDOWS = ("win32", "cygwin") - - -def call_makedirs(cfg, clargs, **kwargs): - """ - Make BL_proxy directories if necessary. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments. - kwargs: dict - MANDATORY: path_i - Dictionary with additional information from previous step. - """ - path_i = kwargs["path_i"] - path_d = map(partial(get_dir, cfg, clargs, **kwargs), path_i) - path_d = tee(chain(*path_d)) - kickstart(map(lambda p: LOGGER.info("Directory @ {}".format(p)), path_d[0])) - if clargs.dry_run: - return - path_d = (os.makedirs(p, exist_ok=True) for p in path_d[1]) - kickstart(path_d) - - -def call(cfg, clargs, *, cmds, **kwargs): - """ - Generic subprocess calls. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments. - cmds: iter(tuple(str)) - kwargs: dict - MANDATORY: path_i - Dictionary with additional information from previous step. - - Returns - ------- - out: str - Stdout & Stderr gathered from subprocess call. - """ - kwargs_s = { - "stdout": sp.PIPE, - "stderr": sp.STDOUT, - "universal_newlines": True, - "check": kwargs.get("check", True), - "shell": kwargs.get("shell", False), - "creationflags": sp.CREATE_NEW_PROCESS_GROUP if sys.platform in WINDOWS else 0, - } - if kwargs_s["shell"]: - cmds = map(lambda cmd: (cmd[0], " ".join(cmd[1])), cmds) - cmds = tee(cmds) - kickstart(map(lambda cmd: LOGGER.debug("CALL :: {}".format(cmd[1])), cmds[0])) - if clargs.dry_run: - return [] - n = len(kwargs["path_i"]) - ps = tqdm( - map(lambda cmd: sp.run(cmd[1], **kwargs_s), cmds[1]), - total=n, - unit="file" if n == 1 else "files", - ) - return [p.stdout for p in ps] diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/commands.py b/power_sequencer/scripts/BPSProxy/bpsproxy/commands.py deleted file mode 100644 index 4a28241b..00000000 --- a/power_sequencer/scripts/BPSProxy/bpsproxy/commands.py +++ /dev/null @@ -1,190 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -import os.path as osp -import shlex as sl -from itertools import chain -from .utils import get_path - - -def get_commands_check(cfg, clargs, **kwargs): - """ - ffprobe subprocess command generation. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments. - cmds: iter(tuple(str)) - kwargs: dict - MANDATORY: path_i_1, path_o_1 - Dictionary with additional information from previous step. - - Returns - ------- - out: iter(tuple(str)) - Iterator containing commands. - """ - cmd = ( - "ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of" - " default=noprint_wrappers=1:nokey=1 '{file}'" - ) - out = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes) - out = map(lambda f: cmd.format(file=f), out) - out = sl.split(cmd.format(file=kwargs["path_i_1"]) + " && " + " && ".join(out)) - return iter((out,)) - - -def get_commands_image_1(cfg, clargs, **kwargs): - """ - ffmpeg subprocess command generation for processing an image. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments. - cmds: iter(tuple(str)) - kwargs: dict - MANDATORY: path_i_1, path_o_1 - Dictionary with additional information from previous step. - - Returns - ------- - out: iter(tuple(str)) - Iterator containing commands. - """ - cmd = "ffmpeg -hwaccel auto -y -v quiet -stats -i '{path_i_1}' {common_all}" - common = "-f apng -filter:v scale=iw*{size}:ih*{size} '{path_o_1}'" - common_all = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes) - common_all = map( - lambda s: common.format(size=s[0] / 100.0, path_o_1=s[1]), zip(clargs.sizes, common_all) - ) - common_all = " ".join(common_all) - out = sl.split(cmd.format(path_i_1=kwargs["path_i_1"], common_all=common_all)) - return iter((out,)) - - -def get_commands_video_1(cfg, clargs, **kwargs): - """ - ffmpeg subprocess command generation for processing a video. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments. - cmds: iter(tuple(str)) - kwargs: dict - MANDATORY: path_i_1, path_o_1 - Dictionary with additional information from previous step. - - Returns - ------- - out: iter(tuple(str)) - Iterator containing commands. - """ - cmd = "ffmpeg -hwaccel auto -y -v quiet -stats -noautorotate -i '{path_i_1}' {common_all}" - common = ( - "-pix_fmt yuv420p" - " -g 1" - " -sn -an" - " -vf colormatrix=bt601:bt709" - " -vf scale=ceil(iw*{size}/2)*2:ceil(ih*{size}/2)*2" - " {preset}" - " '{path_o_1}'" - ) - common_all = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes) - common_all = map( - lambda s: common.format( - preset=cfg["presets"][clargs.preset], size=s[0] / 100.0, path_o_1=s[1] - ), - zip(clargs.sizes, common_all), - ) - common_all = " ".join(common_all) - out = sl.split(cmd.format(path_i_1=kwargs["path_i_1"], common_all=common_all)) - return iter((out,)) - - -def get_commands(cfg, clargs, *, what, **kwargs): - """ - Delegates the creation of commands lists to appropriate functions based on `what` parameter. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments. - cmds: iter(tuple(str)) - what: str - Determines the returned value (see: Returns[out]). - kwargs: dict - MANDATORY: path_i - Dictionary with additional information from previous step. - - Returns - ------- - out: iter(tuple(str, tuple(str))) - An iterator with the 1st element as a tag (the `what` parameter) and the 2nd - element as the iterator of the actual commands. - """ - get_commands_f = { - "video": get_commands_video_1, - "image": get_commands_image_1, - "check": get_commands_check, - } - ps = ( - kwargs["path_i"] - if what not in cfg["extensions"] - else filter( - lambda p: osp.splitext(p)[1].lower() in cfg["extensions"][what], kwargs["path_i"] - ) - ) - ps = map(lambda p: (p, get_path(cfg, clargs, p, **kwargs)), ps) - out = chain.from_iterable( - map(lambda p: get_commands_f[what](cfg, clargs, path_i_1=p[0], path_o_1=p[1], **kwargs), ps) - ) - return map(lambda c: (what, c), out) - - -def get_commands_vi(cfg, clargs, **kwargs): - """ - Delegates the creation of commands lists to appropriate functions for video/image processing. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments. - cmds: iter(tuple(str)) - kwargs: dict - MANDATORY: path_i_1, path_o_1 - Dictionary with additional information from previous step. - - Returns - ------- - out: iter(tuple(str, tuple(str))) - An iterator with the 1st element as a tag (the `what` parameter) and the 2nd - element as the iterator of the actual commands. - """ - ws = filter(lambda x: x is not "all", cfg["extensions"]) - return chain.from_iterable(map(lambda w: get_commands(cfg, clargs, what=w, **kwargs), ws)) diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/config.py b/power_sequencer/scripts/BPSProxy/bpsproxy/config.py deleted file mode 100644 index 28554e85..00000000 --- a/power_sequencer/scripts/BPSProxy/bpsproxy/config.py +++ /dev/null @@ -1,41 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -import multiprocessing as mp -from itertools import chain -import logging as lg - - -CONFIG = { - "logger": "BPS", - "proxy_directory": "BL_proxy", - "proxy_sizes": (25, 50, 100), - "extensions": { - "video": {".mp4", ".mkv", ".mov", ".flv", ".mts"}, - "image": {".png", ".jpg", ".jpeg"}, - }, - "presets": { - "webm": "-c:v libvpx -crf 25 -speed 16 -threads {}".format(str(mp.cpu_count())), - "mp4": "-c:v libx264 -crf 25 -preset faster -tune fastdecode", - "nvenc": "-c:v h264_nvenc -qp 25 -preset fast", - }, - "pre": {"work": "»", "done": "•", "skip": "~"}, -} -CONFIG["extensions"]["all"] = set(chain(*CONFIG["extensions"].values())) - -LOGGER = lg.getLogger(CONFIG["logger"]) -LOGLEV = [lg.INFO, lg.DEBUG] -LOGLEV = [None] + sorted(LOGLEV, reverse=True) diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/utils.py b/power_sequencer/scripts/BPSProxy/bpsproxy/utils.py deleted file mode 100644 index 832a0beb..00000000 --- a/power_sequencer/scripts/BPSProxy/bpsproxy/utils.py +++ /dev/null @@ -1,109 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -""" -Collection of utility functions, class-independent -""" -import os.path as osp -from collections import deque -from shutil import which - - -class ToolError(Exception): - """Raised if external dependencies aren't found on system. - """ - - pass - - -def checktools(tools): - tools = [(t, which(t) or "") for t in tools] - check = {"tools": tools, "test": all(map(lambda x: x[1], tools))} - if not check["test"]: - msg = ["BPSProxy couldn't find external dependencies:"] - msg += [ - "[{check}] {tool}: {path}".format( - check="v" if path is not "" else "X", tool=tool, path=path or "NOT FOUND" - ) - for tool, path in check["tools"] - ] - msg += [ - ( - "Check if you have them properly installed and available in the PATH" - " environemnt variable." - ) - ] - raise ToolError("\n".join(msg)) - - -def get_path_video(cfg, clargs, path, **kwargs): - return osp.join( - osp.dirname(path), cfg["proxy_directory"], osp.basename(path), "proxy_{size}.avi" - ) - - -def get_path_image(cfg, clargs, path, **kwargs): - return osp.join( - osp.dirname(path), - cfg["proxy_directory"], - "images", - "{size}", - "{file}_proxy.jpg".format(file=osp.basename(path)), - ) - - -def get_path(cfg, clargs, path, **kwargs): - get_path_f = {"video": get_path_video, "image": get_path_image} - what = what_vi(cfg, clargs, path, **kwargs) - return get_path_f[what](cfg, clargs, path, **kwargs) - - -def get_dir_video(cfg, clargs, path, **kwargs): - return iter((osp.join(osp.dirname(path), cfg["proxy_directory"], osp.basename(path)),)) - - -def get_dir_image(cfg, clargs, path, **kwargs): - ps = osp.join(osp.dirname(path), cfg["proxy_directory"], "images", "{size}") - return map(lambda s: ps.format(size=s), clargs.sizes) - - -def get_dir(cfg, clargs, path, **kwargs): - get_dir_f = {"video": get_dir_video, "image": get_dir_image} - what = what_vi(cfg, clargs, path, **kwargs) - return get_dir_f[what](cfg, clargs, path, **kwargs) - - -def what_vi(cfg, clargs, p, **kwargs): - return "video" if osp.splitext(p)[1].lower() in cfg["extensions"]["video"] else "image" - - -def kickstart(it): - deque(it, maxlen=0) - - -def printw(cfg, text, s="\n", e="...", p="", **kwargs): - p = p or cfg["pre"]["work"] - print("{s}{p} {}{e}".format(text, s=s, e=e, p=p), **kwargs) - - -def printd(cfg, text, s="", e=".", p="", **kwargs): - p = p or cfg["pre"]["done"] - printw(cfg, text, s=s, e=e, p=p, **kwargs) - - -def prints(cfg, text, s="", e=".", p="", **kwargs): - p = p or cfg["pre"]["skip"] - printw(cfg, text, s=s, e=e, p=p, **kwargs) diff --git a/power_sequencer/scripts/BPSProxy/setup.py b/power_sequencer/scripts/BPSProxy/setup.py deleted file mode 100644 index 9676ef60..00000000 --- a/power_sequencer/scripts/BPSProxy/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -from setuptools import setup - - -def readme(): - with open("README.md") as f: - return f.read() - - -if __name__ == "__main__": - setup( - name="bpsproxy", - version="0.2.1", - description="Blender Power Sequencer proxy generator tool", - long_description=readme(), - long_description_content_type="text/markdown", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Natural Language :: English", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3", - "Topic :: Multimedia :: Video", - "Topic :: Utilities", - ], - url="https://github.com/GDquest/BPSProxy", - keywords="blender proxy vse sequence editor productivity", - author="Răzvan C. Rădulescu", - author_email="razcore.art@gmail.com", - license="GPLv3", - packages=["bpsproxy"], - install_requires=["tqdm"], - zip_safe=False, - entry_points={"console_scripts": ["bpsproxy=bpsproxy.__main__:main"]}, - include_package_data=True, - ) diff --git a/power_sequencer/scripts/BPSRender/bpsrender/__init__.py b/power_sequencer/scripts/BPSRender/bpsrender/__init__.py deleted file mode 100644 index f14cfb6a..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# diff --git a/power_sequencer/scripts/BPSRender/bpsrender/__main__.py b/power_sequencer/scripts/BPSRender/bpsrender/__main__.py deleted file mode 100644 index 07c84fe2..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/__main__.py +++ /dev/null @@ -1,147 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -""" -Renders videos edited in Blender 3D's Video Sequence Editor using multiple CPU -cores. Original script by Justin Warren: -https://github.com/sciactive/pulverize/blob/master/pulverize.py -Modified by sudopluto (Pranav Sharma), gdquest (Nathan Lovato) and -razcore (Razvan Radulescu) - -Under GPLv3 license -""" -import argparse as ap -import os.path as osp -import sys -from functools import partial - -from .calls import call -from .config import CONFIG as C -from .config import LOGGER -from .helpers import BSError, ToolError, checktools, kickstart, prints -from .setup import setup - -# https://github.com/mikeycal/the-video-editors-render-script-for-blender#configuring-the-script -# there seems no easy way to grab the ram usage in a mulitplatform way -# without writing platform dependent code, or by using a python module - -# Most popluar config is 4 cores, 8 GB ram, this is the default for the script -# https://store.steampowered.com/hwsurvey/ - - -def parse_arguments(cfg): - """ - Uses `argparse` to parse the command line arguments. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - - Returns - ------- - out: Namespace - Command line arguments (normalized). - """ - p = ap.ArgumentParser( - description="Multi-process Blender VSE rendering - will attempt to" - " create a folder called `render` inside of the folder" - " containing `blendfile`. Insider `render` another folder called" - " `parts` will be created for storing temporary files. These files" - " will be joined together as the last step to produce the final" - " render which will be stored inside `render` and it will have the" - " same name as `blendfile`" - ) - p.add_argument( - "-o", - "--output", - default=".", - help="Output folder (will contain a `bpsrender` temp folder for" "rendering parts).", - ) - p.add_argument( - "-w", - "--workers", - type=int, - default=cfg["cpu_count"], - help="Number of workers in the pool (for video rendering).", - ) - p.add_argument( - "-v", "--verbose", action="count", default=0, help="Increase verbosity level (eg. -vvv)." - ) - p.add_argument( - "--dry-run", - action="store_true", - help=( - "Run the script without actual rendering or creating files and" - " folders. For DEBUGGING purposes" - ), - ) - p.add_argument("-s", "--start", type=int, default=None, help="Start frame") - p.add_argument("-e", "--end", type=int, default=None, help="End frame") - p.add_argument( - "-m", "--mixdown-only", action="store_true", help="ONLY render the audio MIXDOWN" - ) - p.add_argument( - "-c", - "--concatenate-only", - action="store_true", - help="ONLY CONCATENATE the (already) available video chunks", - ) - p.add_argument( - "-d", - "--video-only", - action="store_true", - help="ONLY render the VIDEO (implies --concatenate-only).", - ) - p.add_argument( - "-j", - "--join-only", - action="store_true", - help="ONLY JOIN the mixdown with the video. This will produce the" " final render", - ) - p.add_argument("blendfile", help="Blender project file to render.") - - clargs = p.parse_args() - clargs.blendfile = osp.abspath(clargs.blendfile) - clargs.output = osp.abspath(clargs.output) - # --video-only implies --concatenate-only - clargs.concatenate_only = clargs.concatenate_only or clargs.video_only - # --dry-run implies maximum verbosity level - clargs.verbose = 99999 if clargs.dry_run else clargs.verbose - return clargs - - -def main(): - """ - Script entry point. - """ - tools = ["blender", "ffmpeg"] - try: - clargs = parse_arguments(C) - checktools(tools) - cmds, kwargs = setup(C, clargs) - kickstart(map(partial(call, C, clargs, **kwargs), cmds)) - except (BSError, ToolError) as e: - LOGGER.error(e) - except KeyboardInterrupt: - # TODO: add actual clean up code - prints(C, "DirtyInterrupt. Exiting", s="\n\n", e="...") - sys.exit() - - -# this is so it can be ran as a module: `python3 -m bpsrender` (for testing) -if __name__ == "__main__": - main() diff --git a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/mixdown.py b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/mixdown.py deleted file mode 100644 index b324333a..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/mixdown.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -import bpy -import os.path as osp -import sys - -if __name__ == "__main__": - for strip in bpy.context.scene.sequence_editor.sequences_all: - if strip.type == "META": - continue - if strip.type != "SOUND": - strip.mute = True - - path = sys.argv[-1] - ext = osp.splitext(path)[1][1:].upper() - bpy.ops.sound.mixdown(filepath=path, check_existing=False, container=ext, codec=ext) diff --git a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/probe.py b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/probe.py deleted file mode 100644 index 92cffa60..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/probe.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -import bpy - -EXT = { - "AVI_JPEG": ".avi", - "AVI_RAW": ".avi", - "FFMPEG": {"MKV": ".mkv", "OGG": ".ogv", "QUICKTIME": ".mov", "AVI": ".avi", "MPEG4": ".mp4"}, -} - -scene = bpy.context.scene - -ext = EXT.get(scene.render.image_settings.file_format, "UNDEFINED") -if scene.render.image_settings.file_format == "FFMPEG": - ext = ext[scene.render.ffmpeg.format] -print("\nBPS:{} {} {}\n".format(scene.frame_start, scene.frame_end, ext)) diff --git a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/video.py b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/video.py deleted file mode 100644 index 590e73eb..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/video.py +++ /dev/null @@ -1,19 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -import bpy - -bpy.context.scene.render.ffmpeg.audio_codec = "NONE" diff --git a/power_sequencer/scripts/BPSRender/bpsrender/calls.py b/power_sequencer/scripts/BPSRender/bpsrender/calls.py deleted file mode 100644 index 5a223dd6..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/calls.py +++ /dev/null @@ -1,410 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -# IMPURE -import multiprocessing as mp -import os -import signal as sig -import subprocess as sp -from functools import partial, reduce -from itertools import chain, islice, starmap, tee -from multiprocessing import Queue - -from tqdm import tqdm - -from .config import LOGGER -from .helpers import BSError, checkblender, kickstart, printd, prints, printw - - -def chunk_frames(cfg, clargs, cmds, **kwargs): - """ - Recover the chunk start/end frames from the constructed commands for the - video step. This is necessary to preserve purity until later steps. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess`. - kwargs: dict - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter(tuple) - Start/end pairs of frames corresponding to the chunk commands created at - the video step. - """ - out = map(lambda x: (x, islice(x, 1, None)), cmds) - out = map(lambda x: zip(*x), out) - out = map(lambda x: filter(lambda y: y[0] in ("-s", "-e"), x), out) - out = map(lambda x: map(lambda y: int(y[1]), x), out) - out = map(lambda x: reduce(lambda acc, y: acc + (y,), x, ()), out) - return out - - -def append_chunks_file(cfg, clargs, cmds, **kwargs): - """ - IMPURE - Helper function for creating the chunks file that will be used by `ffmpeg` - to concatenate the chunks into one video file. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess`. - kwargs: dict - MANDATORY w_frame_start, w_frame_end, ext - Dictionary with additional information from the setup step. - """ - with open(kwargs["chunks_file_path"], "a") as f: - for fs, fe in chunk_frames(cfg, clargs, cmds, **kwargs): - f.write( - "file '{rcp}{fs}-{fe}{ext}'\n".format( - rcp=kwargs["render_chunk_path"].rstrip("#"), - fs="{fs:0{frame_pad}d}".format(fs=fs, **cfg), - fe="{fe:0{frame_pad}d}".format(fe=fe, **cfg), - **kwargs - ) - ) - - -def call_probe(cfg, clargs, cmds, **kwargs): - """ - IMPURE - Probe `clargs.blendfile` for frame start, frame end and extension (for - video only). - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess`. - kwargs: dict - Dictionary with additional information from the setup step. - - Returns - ------- - out: dict - Dictionary with info extracted from `clargs.blendfile`, namely: start - frame, end frame and extension (only useful for video step). - """ - kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True} - - printw(cfg, "Probing") - printw(cfg, "Input(blend) @ {}".format(clargs.blendfile), s="") - frame_start, frame_end, ext = (0, 0, "") - if not clargs.dry_run: - with sp.Popen(next(cmds), **kwargs_p) as cp: - try: - tmp = map(partial(checkblender, "PROBE", [cfg["probe_py"]], cp), cp.stdout) - tmp = filter(lambda x: x.startswith("BPS"), tmp) - tmp = map(lambda x: x[4:].strip().split(), tmp) - frame_start, frame_end, ext = chain(*tmp) - except BSError as e: - LOGGER.error(e) - except KeyboardInterrupt: - raise - finally: - cp.terminate() - returncode = cp.poll() - if returncode != 0: - raise sp.CalledProcessError(returncode, cp.args) - frame_start = frame_start if clargs.start is None else clargs.start - frame_end = frame_end if clargs.end is None else clargs.end - out = { - "frame_start": int(frame_start), - "frame_end": int(frame_end), - "frames_total": int(frame_end) - int(frame_start) + 1, - "ext": ext, - } - if out["ext"] == "UNDEFINED": - raise BSError("Video extension is {ext}. Stopping!".format(ext=ext)) - printd(cfg, "Probing done") - return out - - -def call_mixdown(cfg, clargs, cmds, **kwargs): - """ - IMPURE - Calls blender to render the audio mixdown. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess`. - kwargs: dict - MANDATORY render_mixdown_path - Dictionary with additional information from the setup step. - """ - kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True} - - printw(cfg, "Rendering mixdown") - printw(cfg, "Output @ {}".format(kwargs["render_mixdown_path"]), s="") - if not clargs.dry_run: - with sp.Popen(next(cmds), **kwargs_p) as cp: - try: - tmp = map(partial(checkblender, "MIXDOWN", [cfg["mixdown_py"]], cp), cp.stdout) - tmp = filter(lambda x: x.startswith("BPS"), tmp) - tmp = map(lambda x: x[4:].strip().split(), tmp) - kickstart(tmp) - except BSError as e: - LOGGER.error(e) - except KeyboardInterrupt: - raise - finally: - cp.terminate() - returncode = cp.poll() - if returncode != 0: - raise sp.CalledProcessError(returncode, cp.args) - printd(cfg, "Mixdown done") - - -def call_chunk(cfg, clargs, queue, cmd, **kwargs): - """ - IMPURE - Calls blender to render one chunk (which part is determined by `cmd`). - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmd: tuple - Tuple to be passed to `subprocess`. - kwargs: dict - Dictionary with additional information from the setup step. - """ - sig.signal(sig.SIGINT, sig.SIG_IGN) - kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True} - - if not clargs.dry_run: - # can't use nice functional syntax if we want to simplify with `with` - with sp.Popen(cmd, **kwargs_p) as cp: - try: - tmp = map( - partial( - checkblender, - "VIDEO", - [cfg["video_py"], "The encoder timebase is not set"], - cp, - ), - cp.stdout, - ) - tmp = filter(lambda x: x.startswith("Append frame"), tmp) - tmp = map(lambda x: x.split()[-1], tmp) - tmp = map(int, tmp) - tmp = map(lambda x: True, tmp) - kickstart(map(queue.put, tmp)) - queue.put(False) - except BSError as e: - LOGGER.error(e) - - -def call_video(cfg, clargs, cmds, **kwargs): - """ - IMPURE - Multi-process call to blender for rendering the (video) chunks. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess`. - kwargs: dict - Dictionary with additional information from the setup step. - """ - printw(cfg, "Rendering video (w/o audio)") - printw(cfg, "Output @ {}".format(kwargs["render_chunk_path"]), s="") - try: - not clargs.dry_run and os.remove(kwargs["chunks_file_path"]) - LOGGER.info("CALL-VIDEO: generating {}".format(kwargs["chunks_file_path"])) - except OSError as e: - LOGGER.info("CALL-VIDEO: skipping {}: {}".format(e.filename, e.strerror)) - - cmds, cmds_cf = tee(cmds) - (not clargs.dry_run and append_chunks_file(cfg, clargs, cmds_cf, **kwargs)) - # prepare queue/worker - queues = queues_close = (Queue(),) * clargs.workers - # prpare processes - proc = starmap( - lambda q, cmd: mp.Process(target=partial(call_chunk, cfg, clargs, **kwargs), args=(q, cmd)), - zip(queues, cmds), - ) - # split iterator in 2 for later joining the processes and sum - # one of them - proc, proc_close = tee(proc) - proc = map(lambda p: p.start(), proc) - try: - not clargs.dry_run and kickstart(proc) - - # communicate with processes through the queues and use tqdm to show a - # simple terminal progress bar baesd on video total frames - queues = map(lambda q: iter(q.get, False), queues) - queues = chain(*queues) - queues = tqdm(queues, total=kwargs["frame_end"] - kwargs["frame_start"] + 1, unit="frames") - not clargs.dry_run and kickstart(queues) - except KeyboardInterrupt: - proc_close = map(lambda x: x.terminate(), proc_close) - not clargs.dry_run and kickstart(proc_close) - raise - finally: - # close and join processes and queues - proc_close = map(lambda x: x.join(), proc_close) - not clargs.dry_run and kickstart(proc_close) - - queues_close = map(lambda q: (q, q.close()), queues_close) - queues_close = starmap(lambda q, _: q.join_thread(), queues_close) - not clargs.dry_run and kickstart(queues_close) - printd(cfg, "Video chunks rendering done") - - -def call_concatenate(cfg, clargs, cmds, **kwargs): - """ - IMPURE - Calls ffmpeg in order to concatenate the video chunks together. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess`. - kwargs: dict - MANDATORY: render_video_path - Dictionary with additional information from the setup step. - - Note - ---- - It expects the video chunk files to already be available. - """ - kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL} - printw(cfg, "Concatenating (video) chunks") - printw(cfg, "Output @ {}".format(kwargs["render_video_path"]), s="") - if not clargs.dry_run: - with sp.Popen(next(cmds), **kwargs_p) as cp: - try: - returncode = cp.wait() - if returncode != 0: - raise sp.CalledProcessError(returncode, cp.args) - except KeyboardInterrupt: - raise - finally: - cp.terminate() - printd(cfg, "Concatenating done") - - -def call_join(cfg, clargs, cmds, **kwargs): - """ - IMPURE - Calls ffmpeg for joining the audio mixdown and the video. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess`. - kwargs: dict - MANDATORY: render_audiovideo_path - Dictionary with additional information from the setup step. - - Note - ---- - It expects the audio mixdown and video files to already be available. - """ - kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL} - printw(cfg, "Joining audio/video") - printw(cfg, "Output @ {}".format(kwargs["render_audiovideo_path"]), s="") - if not clargs.dry_run: - with sp.Popen(next(cmds), **kwargs_p) as cp: - try: - returncode = cp.wait() - if returncode != 0: - raise sp.CalledProcessError(returncode, cp.args) - except KeyboardInterrupt: - raise - finally: - cp.terminate() - printd(cfg, "Joining done") - - -def call(cfg, clargs, cmds, **kwargs): - """ - IMPURE - Delegates work to appropriate `call_*` functions. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - cmds: iter(tuple) - Iterator of commands to be passed to `subprocess` - kwargs: dict - MANDATORY: render_audiovideo_path - Dictionary with additional information from the setup step. - - Returns - ------- - out: dict or None - It passes on the output from the `call_*` functions. See `call_*` for - specific details. - - Note - ---- - It tries to be smart and skip steps if child subprocesses give errors. - Example if `--join-only` is passed, but the audio mixdown or video file - aren't available on hard drive. - """ - calls = { - "probe": call_probe, - "mixdown": call_mixdown, - "video": call_video, - "concatenate": call_concatenate, - "join": call_join, - } - try: - out = calls[cmds[0]](cfg, clargs, cmds[1], **kwargs) - return out - except sp.CalledProcessError: - prints( - cfg, - ("WARNING:{}: Something went wrong when calling" " command - SKIPPING").format(cmds[0]), - ) diff --git a/power_sequencer/scripts/BPSRender/bpsrender/commands.py b/power_sequencer/scripts/BPSRender/bpsrender/commands.py deleted file mode 100644 index 910ff021..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/commands.py +++ /dev/null @@ -1,341 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -import math as m -from collections import OrderedDict -from itertools import chain, islice - -from .config import LOGGER - - -def get_commands_probe(cfg, clargs, **kwargs): - """ - Create the command for probing the `clargs.blendfile`. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter(list) - An iterator for which each element is a list to be sent to functions like - `subprocess.run`. - """ - out = ( - "blender", - "--background", - clargs.blendfile, - "--python", - kwargs["probe_py_normalized"], - "--disable-autoexec", - ) - LOGGER.debug("CMD-PROBE: {cmd}".format(cmd=" ".join(out))) - return iter((out,)) - - -def get_commands_chunk(cfg, clargs, **kwargs): - """ - Create the command for rendering a (video) chunk from `clargs.blendfile`. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY render_chunk_path, w_frame_start, w_frame_end - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter(list) - An iterator for which each element is a list to be sent to functions like - `subprocess.run`. - """ - out = ( - "blender", - "--background", - clargs.blendfile, - "--python", - kwargs["video_py_normalized"], - "--disable-autoexec", - "--render-output", - kwargs["render_chunk_path"], - "-s", - str(kwargs["w_frame_start"]), - "-e", - str(kwargs["w_frame_end"]), - "--render-anim", - ) - LOGGER.debug( - "CMD-CHUNK({w_frame_start}-{w_frame_end}): {cmd}".format(cmd=" ".join(out), **kwargs) - ) - return iter((out,)) - - -def get_commands_video(cfg, clargs, **kwargs): - """ - Create the list of commands (one command per chunk) for rendering a video - from `clargs.blendfile`. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY chunk_file_path, frame_start, frame_end, frames_total - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter(tuple) - An iterator for which each element is a tuple to be sent to functions like - `subprocess.run`. - """ - LOGGER.debug("CMD-VIDEO:") - chunk_length = int(m.floor(kwargs["frames_total"] / clargs.workers)) - out = map(lambda w: (w, kwargs["frame_start"] + w * chunk_length), range(clargs.workers)) - out = map( - lambda x: ( - x[1], - x[1] + chunk_length - 1 if x[0] != clargs.workers - 1 else kwargs["frame_end"], - ), - out, - ) - out = map( - lambda x: get_commands( - cfg, clargs, "chunk", w_frame_start=x[0], w_frame_end=x[1], **kwargs - ), - out, - ) - out = map(lambda x: x[1], out) - out = chain(*out) - return tuple(out) - - -def get_commands_mixdown(cfg, clargs, **kwargs): - """ - Create the command to render the mixdown from `clargs.blendfile`. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY render_mixdown_path - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter(tuple) - An iterator for which each element is a tuple to be sent to functions like - `subprocess.run`. - """ - out = ( - "blender --background {blendfile} --python {mixdown_py_normalized}" - " --disable-autoexec -- {render_mixdown_path}".format(**cfg, **vars(clargs), **kwargs) - ) - out = ( - "blender", - "--background", - clargs.blendfile, - "--python", - kwargs["mixdown_py_normalized"], - "--disable-autoexec", - "--", - kwargs["render_mixdown_path"], - ) - LOGGER.debug("CMD-MIXDOWN: {cmd}".format(cmd=" ".join(out))) - return iter((out,)) - - -def get_commands_concatenate(cfg, clargs, **kwargs): - """ - Create the command to concatenate the available video chunks generated - beforehand. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY chunks_file_path, render_video_path - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter(tuple) - An iterator for which each element is a tuple to be sent to functions like - `subprocess.run`. - """ - out = ( - "ffmpeg", - "-stats", - "-f", - "concat", - "-safe", - "-0", - "-i", - kwargs["chunks_file_path"], - "-c", - "copy", - "-y", - kwargs["render_video_path"], - ) - LOGGER.debug("CMD-CONCATENATE: {cmd}".format(cmd=" ".join(out))) - return iter((out,)) - - -def get_commands_join(cfg, clargs, **kwargs): - """ - Create the command to join the available audio mixdown and video generated - beforehand. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY chunks_file_path, render_video_path - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter(tuple) - An iterator for which each element is a tuple to be sent to functions like - `subprocess.run`. - """ - out = ( - "ffmpeg", - "-stats", - "-i", - kwargs["render_video_path"], - "-i", - kwargs["render_mixdown_path"], - "-map", - "0:v:0", - "-c:v", - "copy", - "-map", - "1:a:0", - "-c:a", - "aac", - "-b:a", - "320k", - "-y", - kwargs["render_audiovideo_path"], - ) - LOGGER.debug("CMD-JOIN: {cmd}".format(cmd=" ".join(out))) - return iter((out,)) - - -def get_commands(cfg, clargs, what="", **kwargs): - """ - Delegates the creation of commands lists to appropriate functions based on - `what` parameter. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - what: str (default = '') - Determines the returned value (see: Returns[out]). - kwargs: dict - MANDATORY -- see individual functions for the list of mandatory keys - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter or (str, iter) - |- what == '' is True - An iterator with elements of the type (str) for determining the order in - which to call the functions in the setup step. - NOTE: it skipps the "internal use only" functions. - |- else - A tuple with the 1st element as a tag (the `what` parameter) and the 2nd - element as the iterator of the actual commands. - """ - get_commands_f = OrderedDict( - ( - # internal use only - ("probe", get_commands_probe), - ("chunk", get_commands_chunk), - # direct connection to command line arguments - in order of execution - ("mixdown", get_commands_mixdown), - ("video", get_commands_video), - ("concatenate", get_commands_concatenate), - ("join", get_commands_join), - ) - ) - - return ( - islice(get_commands_f, 2, None) - if what == "" - else (what, get_commands_f[what](cfg, clargs, **kwargs)) - ) - - -def get_commands_all(cfg, clargs, **kwargs): - """ - Prepare the list of commands to be executed depending on the command line - arguments. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY -- see individual functions for the list of mandatory keys - Dictionary with additional information from the setup step. - - Returns - ------- - out: iter((str, tuple)) - An iterator for which each element is a (str, iter(tuple)). The string - value is for tagging the iterator command list (2nd element) for filtering - later based on the given command line arguments. - """ - end = "_only" - out = filter(lambda x: x[0].endswith(end), vars(clargs).items()) - out = map(lambda x: (x[0][: -len(end)], x[1]), out) - order = list(get_commands(cfg, clargs)) - out = sorted(out, key=lambda x: order.index(x[0])) - out = ( - map(lambda k: k[0], out) - if all(map(lambda k: not k[1], out)) - else map(lambda k: k[0], filter(lambda k: k[1], out)) - ) - out = map(lambda k: get_commands(cfg, clargs, k, **kwargs), out) - return out diff --git a/power_sequencer/scripts/BPSRender/bpsrender/config.py b/power_sequencer/scripts/BPSRender/bpsrender/config.py deleted file mode 100644 index b87e3ea3..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/config.py +++ /dev/null @@ -1,37 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -import logging as lg -import multiprocessing as mp -import os.path as osp - -CONFIG = { - "logger": "BPS", - "cpu_count": min(int(mp.cpu_count() / 2), 6), - "bs_path": osp.join(osp.dirname(osp.abspath(__file__)), "bscripts"), - "frame_pad": 7, - "parts_folder": "bpsrender", - "chunks_file": "chunks.txt", - "video_file": "video{}", - "pre": {"work": "»", "done": "•", "skip": "~"}, - "probe_py": "probe.py", - "mixdown_py": "mixdown.py", - "video_py": "video.py", -} - -LOGGER = lg.getLogger(CONFIG["logger"]) -LOGLEV = [lg.INFO, lg.DEBUG] -LOGLEV = [None] + sorted(LOGLEV, reverse=True) diff --git a/power_sequencer/scripts/BPSRender/bpsrender/helpers.py b/power_sequencer/scripts/BPSRender/bpsrender/helpers.py deleted file mode 100644 index 9ebcf2b0..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/helpers.py +++ /dev/null @@ -1,110 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -from collections import deque -from shutil import which - - -class BSError(Exception): - """ - Custom Exception raised if Blender is called with a python script argument - and gives error while trying to execute the script. - """ - - pass - - -class ToolError(Exception): - """Raised if external dependencies aren't found on system. - """ - - pass - - -def checktools(tools): - tools = [(t, which(t) or "") for t in tools] - check = {"tools": tools, "test": all(map(lambda x: x[1], tools))} - if not check["test"]: - msg = ["BPSRender couldn't find external dependencies:"] - msg += [ - "[{check}] {tool}: {path}".format( - check="v" if path is not "" else "X", tool=tool, path=path or "NOT FOUND" - ) - for tool, path in check["tools"] - ] - msg += [ - ( - "Check if you have them properly installed and available in the PATH" - " environemnt variable." - ), - "Exiting...", - ] - raise ToolError("\n".join(msg)) - - -def checkblender(what, search, cp, s): - """ - IMPURE - Check Blender output for python script execution error. - - Parameters - ---------- - what: str - A tag used in the exception message. - search: iter(str) - One or more string(s) to search for in Blender's output. - cp: Popen - Blender subprocess. - s: PIPE - Blender's output. - - Returns - ------- - out: PIPE - The same pipe `s` is returned so that it can be iterated over on later - steps. - """ - if not isinstance(search, list): - search = [search] - for search_item in search: - if search_item in s: - message = ( - "Script {what} was not properly executed in" " Blender".format(what=what), - "CMD: {cmd}".format(what=what, cmd=" ".join(cp.args)), - "DUMP:".format(what=what), - s, - ) - raise BSError("\n".join(message)) - return s - - -def printw(cfg, text, s="\n", e="...", p="", **kwargs): - p = p or cfg["pre"]["work"] - print("{s}{p} {}{e}".format(text, s=s, e=e, p=p), **kwargs) - - -def printd(cfg, text, s="", e=".", p="", **kwargs): - p = p or cfg["pre"]["done"] - printw(cfg, text, s=s, e=e, p=p, **kwargs) - - -def prints(cfg, text, s="", e=".", p="", **kwargs): - p = p or cfg["pre"]["skip"] - printw(cfg, text, s=s, e=e, p=p, **kwargs) - - -def kickstart(it): - deque(it, maxlen=0) diff --git a/power_sequencer/scripts/BPSRender/bpsrender/setup.py b/power_sequencer/scripts/BPSRender/bpsrender/setup.py deleted file mode 100644 index aba30d07..00000000 --- a/power_sequencer/scripts/BPSRender/bpsrender/setup.py +++ /dev/null @@ -1,182 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -# IMPURE -import logging as lg -import os -import os.path as osp -from functools import reduce -from itertools import starmap - -from .calls import call -from .commands import get_commands, get_commands_all -from .config import LOGGER, LOGLEV -from .helpers import kickstart - - -def setup_bspy(cfg, clargs, **kwargs): - """ - Normalize the names of the script to be ran in Blender for certain steps. - Eg. the probe step depends on the script located in - `bpsrender/cfg['probe_py']`. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - - Returns - ------- - out: dict - Dictoinary to be used in call steps. - """ - out = filter(lambda x: x[0].endswith("_py"), cfg.items()) - out = starmap(lambda k, v: ("{}_normalized".format(k), osp.join(cfg["bs_path"], v)), out) - return dict(out) - - -def setup_probe(cfg, clargs, **kwargs): - """ - IMPURE - Call Blender and extract information that will be necessary for later - steps. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY -- see individual functions for the list of mandatory keys - Dictionary with additional information from the previous setup step. - - Returns - ------- - out: dict - Dictoinary to be used in call steps. - """ - return call(cfg, clargs, get_commands(cfg, clargs, "probe", **kwargs), **kwargs) - - -def setup_paths(cfg, clargs, **kwargs): - """ - Figure out appropriate path locations to store output for parts and final - render. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - MANDATORY -- see individual functions for the list of mandatory keys - Dictionary with additional information from the previous setup step. - - Returns - ------- - out: dict - Dictionary storing all relevant information pertaining to folder and file - paths. - - Note - ---- - It also creates the folder structure 'render/parts' where - `clargs.blendfile` is stored on disk. - """ - render_parts_path = osp.join(clargs.output, cfg["parts_folder"]) - name = osp.splitext(osp.basename(clargs.blendfile))[0] - render_mixdown_path = osp.join(render_parts_path, "{}_m.flac".format(name)) - render_chunk_path = osp.join(render_parts_path, "{}_c_{}".format(name, "#" * cfg["frame_pad"])) - render_video_path = osp.join(render_parts_path, "{}_v{}".format(name, kwargs["ext"])) - render_audiovideo_path = osp.join(clargs.output, "{}{}".format(name, kwargs["ext"])) - chunks_file_path = osp.join(render_parts_path, cfg["chunks_file"]) - - out = { - "render_path": clargs.output, - "render_parts_path": render_parts_path, - "chunks_file_path": chunks_file_path, - "render_chunk_path": render_chunk_path, - "render_video_path": render_video_path, - "render_mixdown_path": render_mixdown_path, - "render_audiovideo_path": render_audiovideo_path, - } - return out - - -def setup_folders_hdd(cfg, clargs, **kwargs): - """ - IMPURE - Prepares the folder structure `cfg['render']/cfg['parts']'`. - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - Dictionary with additional information from the previous setup step. - - Returns - ------- - out: (iter((str, iter(tuple))), dict) - 1st element: see commands.py:get_commands_all - 2nd elment: the keyword arguments used by calls.py:call - """ - # create folder structure if it doesn't exist already only if - # appropriate command line arguments are given - do_it = filter(lambda x: x[0].endswith("_only"), vars(clargs).items()) - do_it = all(map(lambda x: not x[1], do_it)) - do_it = not clargs.dry_run and clargs.video_only or clargs.mixdown_only or do_it - do_it and os.makedirs(kwargs["render_parts_path"], exist_ok=True) - return {} - - -def setup(cfg, clargs): - """ - IMPURE -- setup_paths - Prepares the folder structure 'render/parts', the appropriate command lists - to be called and the keyword arguments to be passed to call functions - (calls.py). - - Parameters - ---------- - cfg: dict - Configuration dictionary. - clargs: Namespace - Command line arguments (normalized). - kwargs: dict - Dictionary with additional information from the previous setup step. - - Returns - ------- - out: (iter((str, iter(tuple))), dict) - 1st element: see commands.py:get_commands_all - 2nd elment: the keyword arguments used by calls.py:call - """ - setups_f = (setup_bspy, setup_probe, setup_paths, setup_folders_hdd) - lg.basicConfig(level=LOGLEV[min(clargs.verbose, len(LOGLEV) - 1)]) - - kwargs = dict(reduce(lambda acc, sf: {**acc, **sf(cfg, clargs, **acc)}, setups_f, {})) - - LOGGER.info("Setup:") - kickstart(starmap(lambda k, v: LOGGER.info("{}: {}".format(k, v)), kwargs.items())) - return get_commands_all(cfg, clargs, **kwargs), kwargs diff --git a/power_sequencer/scripts/BPSRender/setup.py b/power_sequencer/scripts/BPSRender/setup.py deleted file mode 100644 index 1f17b9ba..00000000 --- a/power_sequencer/scripts/BPSRender/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -# -# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors -# -# This file is part of Power Sequencer. -# -# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the -# GNU General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with Power Sequencer. If -# not, see <https://www.gnu.org/licenses/>. -# -from setuptools import setup - - -def readme(): - with open("README.rst") as f: - return f.read() - - -if __name__ == "__main__": - setup( - name="bpsrender", - version="0.1.40.post1", - description="Blender Power Sequencer Renderer", - long_description=readme(), - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Natural Language :: English", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Topic :: Text Processing :: Linguistic", - "Topic :: Multimedia :: Video", - "Topic :: Utilities", - ], - url="https://gitlab.com/razcore/BPSRender", - keywords="blender render parallel multiprocess speedup utility" " productivty", - author="Răzvan C. Rădulescu", - author_email="razcore.art@gmail.com", - license="GPLv3", - packages=["bpsrender"], - install_requires=["tqdm"], - zip_safe=False, - entry_points={"console_scripts": ["bpsrender=bpsrender.__main__:main"]}, - include_package_data=True, - ) |