and though bugs are the bane of my existence, rest assured the wretched thing will get the best of care here

...
 
Commits (5)
image: alpine/latest
packages:
- py3-pip
sources:
- https://git.sr.ht/~boringcactus/vidslice
tasks:
- install: |
cd vidslice
python3 -m pip install -r requirements.txt
- build: |
cd vidslice
python3 -m zipapp vidslice -o vidslice-dev.pyw -c
artifacts:
- vidslice/vidslice-dev.pyw
language: shell
if: tag IS present OR branch = build-fuckery
dist: bionic
os:
- linux
- osx
- windows
addons:
apt:
packages:
- python3.7
- python3.7-dev
- python3-pip
- python3-setuptools
- python3-tk
homebrew:
packages:
- python@3.7
before_install:
- chmod +x scripts/setup_${TRAVIS_OS_NAME}.sh
- . scripts/setup_${TRAVIS_OS_NAME}.sh
install:
- which python && export PYTHON=python || echo whatever
- which python3 && export PYTHON=python3 || echo whatever
- which python3.7 && export PYTHON=python3.7 || echo whatever
- echo ${PYTHON}
- ${PYTHON} -m pip install -r requirements.txt
script:
- ${PYTHON} setup.py build
before_deploy:
- ${PYTHON} setup.py build
- pushd build
- mv * vidslice
- zip vidslice-${TRAVIS_OS_NAME}.zip -r vidslice || powershell Compress-Archive -Path vidslice -DestinationPath vidslice-${TRAVIS_OS_NAME}.zip
- ls
- popd
deploy:
skip_cleanup: true
provider: releases
api_key:
secure: "MEE1jkxa+YX50iYLrDHUUdW4anlDwhgT41quWqCCM2qRg3iY4qaN/BK5Q4XSd+h8RCf2gDRAU3wP6jA6cUrxFH6Nw/hszgvXN90A2Lhs2EG5HQ4bVOqzvsO13Zf8/Ha0PJyTXFSnRCdGUpSggcuJ3zbhSoU5xNO+Ch7WWcGraYuoCGS0Xl24LvQzWCb400C1tifx9ITDEinPYHR7objlRmmitvy6jhWmfiP2zeefTXAzLvYWhR3DpvsuzriVR79AVriRwnFP1yjpjfpvxnjqx+3GJEYmQy1SmaiQa8f9jK+agLyszefRoyquMu6j2pYTw9CPp7LC1WyVFHV1k0d74l2v9Yr2sVDqx6i131+9bhSD9D0OdL8A1RuPUlJUWy0FncfRUj3+rL3aCf+qO2kYS1gGeTD3A9RuaMYFsrrNqDtsLA1reF0nQ3eGqJ3HIaSyTFPt8EJ5bG8H2qErnGRz5E/TEDoZRY5rIyKwnIaPzoMLyRJGn7I/5ufvb0B06BNfhQLS4HrjqVTYFjM2ulbwTPLxmZH60hw/SAWpWFFhdbAWpNn7ezV9IBQEb2N+ovAPUgTbcWio1FYSZyB81NVgfL7V/aGIEmB0KNYvB6JL6VEaGV1BCf63X/5Nl7qxXZRBlGjwE893NKfNuxCjU0QC8voA3DwUXYmLr69PusyemtY="
file_glob: true
file: build/*.zip
on:
repo: boringcactus/vidslice
tags: true
#!/usr/bin/env bash
choco install python --version 3.7.9 -y
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
export PATH=/C/Python37:$PATH
python get-pip.py
import os
import sys
from cx_Freeze import setup, Executable
from vidslice import VERSION
# Dependencies are automatically detected, but it might need
# fine tuning.
buildOptions = dict(packages=[], excludes=[])
PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
if sys.platform == "win32":
dlls_folder = os.path.join(PYTHON_INSTALL_DIR, 'DLLs')
targets = ['libcrypto', 'libssl']
include_files = []
for dll in os.listdir(dlls_folder):
for target in targets:
if target.startswith(target):
include_files.append(os.path.join(dlls_folder, dll))
buildOptions['include_files'] = include_files
base = 'Win32GUI' if sys.platform == 'win32' else None
executables = [
Executable('vidslice.py', base=base)
]
setup(name='vidslice',
version=VERSION,
description='',
options=dict(build_exe=buildOptions),
executables=executables)
......@@ -7,6 +7,7 @@ from tkinter import ttk
from options import OptionsPanel
from output import OutputPanel
from preview import PreviewPanel
from sources import SourcesPanel, update_ytdl
VERSION = "1.6"
......@@ -45,20 +46,24 @@ class VidsliceFrame:
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# set up sources panel
self.sources_panel = SourcesPanel(mainframe)
self.sources_panel.grid(column=0, row=0, columnspan=2, sticky=(W, E), padx=5, pady=5)
self.sources_panel.grid(column=0, row=0, columnspan=2, sticky=(W, E, N, S), padx=5, pady=5)
# set up options panel
self.options_panel = OptionsPanel(mainframe)
self.options_panel.grid(column=0, row=1, sticky=(W, E, N), padx=5, pady=5)
mainframe.rowconfigure(1, weight=1)
self.options_panel.grid(column=0, row=1, columnspan=2, sticky=(W, N, S), padx=5, pady=5)
self.sources_panel.on_update(self.options_panel.update_info)
# set up output panel
self.preview_panel = PreviewPanel(mainframe, get_ffmpeg_args=self.options_panel.ffmpeg_opts,
get_frame_count=self.options_panel.frame_count)
self.preview_panel.grid(column=0, row=2, sticky=(W, E, N, S), padx=5, pady=5)
mainframe.rowconfigure(2, weight=1)
mainframe.columnconfigure(0, weight=2)
self.sources_panel.on_update(
lambda data: self.preview_panel.set_input_path(self.sources_panel.get_file(), data))
self.output_panel = OutputPanel(mainframe, get_ffmpeg_args=self.options_panel.ffmpeg_opts,
get_frame_count=self.options_panel.frame_count)
self.output_panel.grid(column=1, row=1, sticky=(W, E, N, S), padx=5, pady=5)
self.output_panel.grid(column=1, row=2, sticky=(W, E, N, S), padx=5, pady=5)
mainframe.columnconfigure(1, weight=1)
self.sources_panel.on_update(lambda data: self.output_panel.set_input_path(self.sources_panel.get_file(), data))
......
......@@ -9,6 +9,10 @@ DURATION_ROW = 3
WIDTH_ROW = 4
HEIGHT_ROW = 5
FRAMERATE_ROW = 6
CROP_TOP_ROW = 7
CROP_BOTTOM_ROW = 8
CROP_LEFT_ROW = 9
CROP_RIGHT_ROW = 10
LABEL_COL = 0
ORIG_COL = 1
EDIT_BOX_COL = 2
......@@ -16,9 +20,16 @@ NEW_COL = 3
class FFmpegOptions:
def __init__(self, input, output):
def __init__(self, input, output, vf):
self.input = input
self.output = output
self.vf = vf
def output_with_vf(self):
if len(self.vf) > 0:
return self.output + ['-vf', ','.join(self.vf)]
else:
return self.output
class Property:
......@@ -125,6 +136,18 @@ class OptionsPanel(ttk.LabelFrame):
self.framerate = Property(self, "Framerate", FRAMERATE_ROW, float)
self.framerate.on_change(self.enforce_constraints)
self.crop_top = Property(self, "Crop Top", CROP_TOP_ROW, int)
self.crop_top.on_change(self.enforce_constraints)
self.crop_bottom = Property(self, "Crop Bottom", CROP_BOTTOM_ROW, int)
self.crop_bottom.on_change(self.enforce_constraints)
self.crop_left = Property(self, "Crop Left", CROP_LEFT_ROW, int)
self.crop_left.on_change(self.enforce_constraints)
self.crop_right = Property(self, "Crop Right", CROP_RIGHT_ROW, int)
self.crop_right.on_change(self.enforce_constraints)
for child in self.winfo_children():
child.grid_configure(padx=2, pady=2)
......@@ -186,6 +209,16 @@ class OptionsPanel(ttk.LabelFrame):
self.width.set_calc_new(round(orig_width / orig_height * new_height))
self.height.set_calc_new(round(orig_height / orig_width * new_width))
self.crop_top.set_calc_new(0)
self.crop_top.set_range(0, int(self.height.get_final()) - int(self.crop_bottom.get_final()))
self.crop_bottom.set_calc_new(0)
self.crop_bottom.set_range(0, int(self.height.get_final()) - int(self.crop_top.get_final()))
self.crop_right.set_calc_new(0)
self.crop_right.set_range(0, int(self.width.get_final()) - int(self.crop_left.get_final()))
self.crop_left.set_calc_new(0)
self.crop_left.set_range(0, int(self.width.get_final()) - int(self.crop_right.get_final()))
if self.framerate.is_enabled():
orig_framerate = float(self.framerate.get_orig())
self.framerate.set_range(0, orig_framerate)
......@@ -215,6 +248,14 @@ class OptionsPanel(ttk.LabelFrame):
self.width.set_orig(video_stream['width'])
self.height.enable()
self.height.set_orig(video_stream['height'])
self.crop_top.enable()
self.crop_top.set_orig(0)
self.crop_bottom.enable()
self.crop_bottom.set_orig(0)
self.crop_left.enable()
self.crop_left.set_orig(0)
self.crop_right.enable()
self.crop_right.set_orig(0)
framerate = round(float(fractions.Fraction(video_stream['avg_frame_rate'])), 3)
self.framerate.enable()
......@@ -223,6 +264,10 @@ class OptionsPanel(ttk.LabelFrame):
self.width.disable()
self.height.disable()
self.framerate.disable()
self.crop_top.disable()
self.crop_bottom.disable()
self.crop_left.disable()
self.crop_right.disable()
self.state(['!disabled'])
self.enforce_constraints()
......@@ -230,6 +275,7 @@ class OptionsPanel(ttk.LabelFrame):
def ffmpeg_opts(self):
input_opts = []
output_opts = []
vf = []
if self.start_time.is_edit():
input_opts += ['-ss', str(self.start_time.get_final())]
......@@ -250,12 +296,18 @@ class OptionsPanel(ttk.LabelFrame):
width = "-1"
if not self.height.is_edit():
height = "-1"
output_opts += ['-vf', 'scale=' + width + ':' + height]
vf += ['scale=' + width + ':' + height]
if self.crop_top.is_edit() or self.crop_bottom.is_edit() or \
self.crop_left.is_edit() or self.crop_right.is_edit():
out_w = int(self.width.get_final()) - int(self.crop_left.get_final()) - int(self.crop_right.get_final())
out_h = int(self.height.get_final()) - int(self.crop_top.get_final()) - int(self.crop_bottom.get_final())
vf += [f'crop={out_w}:{out_h}:{self.crop_left.get_final()}:{self.crop_top.get_final()}']
if self.framerate.is_edit():
output_opts += ['-r', str(self.framerate.get_final())]
return FFmpegOptions(input_opts, output_opts)
return FFmpegOptions(input_opts, output_opts, vf)
def frame_count(self):
return float(self.duration.get_final()) * float(self.framerate.get_final())
......@@ -9,7 +9,7 @@ from options import FFmpegOptions
class OutputPanel(ttk.LabelFrame):
def __init__(self, *args, get_ffmpeg_args=lambda: FFmpegOptions([], []), get_frame_count=lambda: 0, **kw):
def __init__(self, *args, get_ffmpeg_args=lambda: FFmpegOptions([], [], []), get_frame_count=lambda: 0, **kw):
super(OutputPanel, self).__init__(*args, text='Output', **kw)
self.update_listeners = []
self.input_path = None
......@@ -90,7 +90,7 @@ class OutputPanel(ttk.LabelFrame):
self.progress['maximum'] = float(self.get_frame_count())
print(self.get_frame_count())
input_args = real_args.input
output_args = real_args.output
output_args = real_args.output_with_vf()
output_path = self.file_text.get()
(folder, name) = os.path.split(output_path)
(name, ext) = os.path.splitext(name)
......@@ -131,6 +131,7 @@ class OutputPanel(ttk.LabelFrame):
self.progress['value'] = float(progress_data.group(1))
else:
self.logs.set(self.logs.get() + out_data)
self.progress['value'] = self.progress['maximum']
self.enable(True)
callback(proc.returncode)
......@@ -153,8 +154,9 @@ class OutputPanel(ttk.LabelFrame):
self.handle_run_pressed(*args, callback=quit)
def set_input_path(self, path, data):
self.enable(data is not None)
if data is None:
self.enable(False)
self.input_path = None
else:
self.handle_file_changed()
self.input_path = path
import subprocess
import tempfile
import threading
from tkinter import *
from tkinter import ttk
from options import FFmpegOptions
class PreviewPanel(ttk.LabelFrame):
def __init__(self, *args, get_ffmpeg_args=lambda: FFmpegOptions([], []), get_frame_count=lambda: 0, **kw):
super(PreviewPanel, self).__init__(*args, text='Preview', **kw)
self.input_path = None
self.get_ffmpeg_args = get_ffmpeg_args
self.get_frame_count = get_frame_count
def button(text, command, column):
ttk.Button(self, text=text, command=command).grid(column=column, row=0, sticky=(N, W, S, E))
self.columnconfigure(column, weight=1)
button("Preview Start", self.preview_start, 0)
button("Preview Middle", self.preview_middle, 1)
button("Preview End", self.preview_end, 2)
self.image = None
self.image_label = ttk.Label(self, anchor='center')
self.image_label.grid(column=0, row=1, columnspan=3, sticky=(N, W, S, E))
self.rowconfigure(1, weight=1)
self.enable(False)
def preview_at(self, offset):
offset = int(offset)
self.enable(False)
real_args = self.get_ffmpeg_args()
input_args = real_args.input
width = self.image_label.winfo_width()
height = self.image_label.winfo_height()
real_args.vf += [
rf'select=eq(n\,{offset})',
f'scale=w={width}:h={height}:force_original_aspect_ratio=decrease'
]
real_args.output += ['-frames:v', '1']
output_args = real_args.output_with_vf()
def run():
_, output_path = tempfile.mkstemp(suffix='.png')
args = ['ffmpeg', '-hide_banner', '-v', 'warning', '-y'] + input_args + \
['-i', self.input_path] + output_args + [output_path]
print(args)
# noinspection PyArgumentList
proc = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
while proc.poll() is None:
out_data = proc.stdout.readline()
if out_data != '':
print(out_data, end='')
self.enable(True)
self.image = PhotoImage(file=output_path)
self.image_label['image'] = self.image
threading.Thread(target=run).start()
def preview_start(self, *args):
self.preview_at(0)
def preview_middle(self, *args):
self.preview_at(self.get_frame_count() / 2)
def preview_end(self, *args):
self.preview_at(self.get_frame_count() - 1)
def enable(self, enabled):
state = 'disabled'
if enabled:
state = '!' + state
self.state([state])
for child in self.winfo_children():
try:
child.state([state])
except AttributeError:
pass
def set_input_path(self, path, data):
self.enable(data is not None)
if data is None:
self.input_path = None
else:
self.input_path = path