From 9490e7ed02bf88a3993ff9e5628f7a99e89f3c92 Mon Sep 17 00:00:00 2001 From: ekisu Date: Sun, 13 Sep 2026 22:54:41 -0300 Subject: [PATCH 1/2] Add catbox/litterbox encode & upload flow (u) Implements #228 for the Catbox services, which share an anonymous multipart upload API: - New `u: encode & upload` action on the main page, next to `e: encode`. It runs the normal encoder and then uploads the resulting file. - Destination is configured in the options (`Upload Destination`) and in webm.conf: `upload_host` (catbox | litterbox), `litterbox_time`, `catbox_userhash`, `upload_curl_path`, `open_after_upload`. - New UploadWithProgress page: async curl subprocess with live progress (ESC cancels), plus done and failed states. The resulting URL is copied to the clipboard. - src/upload.moon holds the host adapters, curl argument builder and the clipboard/browser helpers. The `u` line is hidden when curl is missing. - Streamable is intentionally not offered: its upload API requires an account (HTTP 401) and its web upload routes return 404. - Offline integration tests use a stub curl to cover both hosts and the missing-range abort path. --- Makefile | 2 + README.md | 12 +++ src/EncodeOptionsPage.moon | 4 + src/MainPage.moon | 34 ++++++- src/UploadWithProgress.moon | 156 +++++++++++++++++++++++++++++++++ src/encode.moon | 9 +- src/options.lua | 13 +++ src/testing.moon | 1 + src/upload.moon | 100 +++++++++++++++++++++ tests/testcases/test_upload.py | 99 +++++++++++++++++++++ 10 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 src/UploadWithProgress.moon create mode 100644 src/upload.moon create mode 100644 tests/testcases/test_upload.py diff --git a/Makefile b/Makefile index 9e8b394..fd58d5a 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ SOURCES += src/testing.moon SOURCES += src/util.moon SOURCES += src/video_to_screen.moon SOURCES += src/vp8_twopass_log_patcher.moon +SOURCES += src/upload.moon SOURCES += src/formats/base.moon SOURCES += src/formats/rawvideo.moon SOURCES += src/formats/webm.moon @@ -18,6 +19,7 @@ SOURCES += src/formats/gif.moon SOURCES += src/formats/webp.moon SOURCES += src/Page.moon SOURCES += src/EncodeWithProgress.moon +SOURCES += src/UploadWithProgress.moon SOURCES += src/encode.moon SOURCES += src/CropPage.moon SOURCES += src/EncodeOptionsPage.moon diff --git a/README.md b/README.md index fcffac2..b7f5448 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,18 @@ By default, the script is activated by the W (shift+w) key. ## Usage Follow the on-screen instructions. Encoded WebM files will have audio/subs based on the current playback options (i.e. will be muted if no audio, won't have hardcoded subs if subs aren't visible). +### Uploading + +Press `u` (encode & upload) on the WebM maker page to encode a clip and upload it in one step. The destination is set in the options (`o`) and stored in `webm.conf`: + +- `upload_host` — `catbox` (permanent) or `litterbox` (temporary). +- `litterbox_time` — expiry for litterbox uploads: `1h`, `12h`, `24h` or `72h`. +- `catbox_userhash` — optional Catbox account hash; account uploads are permanent and manageable. +- `upload_curl_path` — the `curl` executable used for the multipart upload. +- `open_after_upload` — open the link in the browser as soon as it is ready. + +Uploads use `curl`; when it is not on `PATH`, the `u` line is hidden. The resulting URL is copied to the clipboard (mpv's `clipboard/text` when available, otherwise `wl-copy`, `xclip` or `pbcopy`). Only the Catbox services are supported: Streamable requires an account for uploads, so it is not offered. + ## Configuration You can configure the script's defaults by either changing the `options` at the beginning of the script, or placing a `webm.conf` inside the `script-opts` directory. A sample `webm.conf` file with the default options can be found [here][conf]. Note that you don't need to specify all options, only the ones you wish to override. diff --git a/src/EncodeOptionsPage.moon b/src/EncodeOptionsPage.moon index a774f92..65db282 100644 --- a/src/EncodeOptionsPage.moon +++ b/src/EncodeOptionsPage.moon @@ -158,6 +158,9 @@ class EncodeOptionsPage extends Page formatOpts = possibleValues: [{fId, formats[fId].displayName} for fId in *formatIds] + uploadHostOpts = + possibleValues: upload_host_possible_values! + gifDitherOpts = possibleValues: {{0, "bayer_scale 0"}, {1, "bayer_scale 1"}, {2, "bayer_scale 2"}, {3, "bayer_scale 3"}, {4, "bayer_scale 4"}, {5, "bayer_scale 5"}, {6, "sierra2_4a"}} @@ -166,6 +169,7 @@ class EncodeOptionsPage extends Page -- by dicts on Lua. @options = { {"output_format", Option("list", "Output Format", options.output_format, formatOpts)} + {"upload_host", Option("list", "Upload Destination", options.upload_host, uploadHostOpts, -> is_upload_available!)} {"twopass", Option("bool", "Two Pass", options.twopass)}, {"apply_current_filters", Option("bool", "Apply Current Video Filters", options.apply_current_filters)} {"scale_height", Option("list", "Scale Height", options.scale_height, scaleHeightOpts)}, diff --git a/src/MainPage.moon b/src/MainPage.moon index 4830434..c35492a 100644 --- a/src/MainPage.moon +++ b/src/MainPage.moon @@ -9,6 +9,7 @@ class MainPage extends Page "o": self\changeOptions "p": self\preview "e": self\encode + "u": self\upload "ESC": self\hide @startTime = -1 @endTime = -1 @@ -60,7 +61,9 @@ class MainPage extends Page ass\append("#{bold('@:')} jump to end time\\N") ass\append("#{bold('o:')} change encode options\\N") ass\append("#{bold('p:')} preview\\N") - ass\append("#{bold('e:')} encode\\N\\N") + ass\append("#{bold('e:')} encode\\N") + ass\append("#{bold('u:')} encode & upload\\N") if is_upload_available! + ass\append("\\N") ass\append("#{bold('ESC:')} close\\N") mp.set_osd_ass(window_w, window_h, ass.text) @@ -95,6 +98,35 @@ class MainPage extends Page previewPage = PreviewPage(self\onPreviewEnded, @region, @startTime, @endTime) previewPage\show! + onEncodedForUpload: (success, outPath) => + if not success or not outPath + self\show! + return + uploadPage = UploadWithProgress(self\onUploadEnded, outPath) + uploadPage\show! + + onUploadEnded: (state) => + if state == "options" + self\changeOptions! + else + self\show! + + upload: => + if not is_upload_available! + message("Uploads need curl on PATH. Set upload_curl_path in webm.conf.") + return + if @startTime < 0 + message("No start time, aborting") + return + if @endTime < 0 + message("No end time, aborting") + return + if @startTime >= @endTime + message("Start time is ahead of end time, aborting") + return + self\hide! + encode(@region, @startTime, @endTime, self\onEncodedForUpload) + encode: => self\hide! if @startTime < 0 diff --git a/src/UploadWithProgress.moon b/src/UploadWithProgress.moon new file mode 100644 index 0000000..a1b9dea --- /dev/null +++ b/src/UploadWithProgress.moon @@ -0,0 +1,156 @@ +-- Progress, done and failed states for a single upload. Not an encode page: it +-- runs the curl subprocess asynchronously so ESC can abort it and the progress +-- percentage keeps redrawing. +class UploadWithProgress extends Page + -- callback(state) where state is "done", "failed", "cancelled" or "options". + new: (callback, path) => + @callback = callback + @path = path + _, @filename = utils.split_path(path) + info = utils.file_info(path) + @sizeText = info and string.format("%.1f MB", info.size / 1000000) or "unknown size" + @responsePath = os.tmpname() + @progressPath = os.tmpname() + @state = "uploading" + @percent = 0 + @finished = false + @cancelled = false + @keybinds = + "ESC": self\onEscape + "c": self\copyAgain + "o": self\openSomething + "r": self\retry + + prepare: => + self\startUpload! + + dispose: => + self\stopTimer! + os.remove(@responsePath) if @responsePath + os.remove(@progressPath) if @progressPath + + stopTimer: => + if @timer + @timer\kill! + @timer = nil + + startUpload: => + @state = "uploading" + @percent = 0 + @finished = false + @cancelled = false + @host = get_upload_host(options.upload_host) + + -- Clear a stale progress meter from a previous attempt. + file = io.open(@progressPath, "w") + file\close! if file + + args = build_upload_args(@path, options.upload_host, @responsePath, @progressPath) + @timer = mp.add_periodic_timer(0.2, self\pollProgress) + @handle = mp.command_native_async({ + name: "subprocess" + args: args + playback_only: false + capture_stdout: true + capture_stderr: false + capture_size: 128 + }, self\onFinished) + + pollProgress: => + return if @state != "uploading" + content = read_file(@progressPath) + return if not content or content == "" + percent = nil + for match in content\gmatch("(%d+%.?%d*)%%") + percent = tonumber(match) + if percent and math.floor(percent) != @percent + @percent = math.floor(percent) + self\draw! + + onFinished: (success, result, error) => + return if @finished + @finished = true + self\stopTimer! + + if @cancelled + emit_event("upload-finished", "cancelled") + self\finish("cancelled") + return + + httpCode = result and tonumber(result.stdout) + response = trim(read_file(@responsePath) or "") + uploadOk = success and result and result.status == 0 and + httpCode and httpCode >= 200 and httpCode < 300 and + response\match("^https?://") + + if uploadOk + @url = response + copy_to_clipboard(@url) + open_url(@url) if options.open_after_upload + @state = "done" + else + @state = "failed" + @errorMessage = response + if @errorMessage == "" + @errorMessage = (result and result.error_string) or tostring(error) or "upload failed" + @errorMessage = @errorMessage\gsub("%s+", " ")\sub(1, 300) + emit_event("upload-finished", @state) + self\draw! + + onEscape: => + if @state == "uploading" + @cancelled = true + self\stopTimer! + mp.abort_async_command(@handle) if @handle + -- If the abort never reports back, close anyway. + mp.add_timeout(0.5, (-> self\finish("cancelled") if not @closing)) + else + self\finish(@state) + + -- Only meaningful on the done page. + copyAgain: => + if @state == "done" and @url + copy_to_clipboard(@url) + message("Link copied to clipboard.") + + -- Open the result on the done page; jump to the options on the failed page. + openSomething: => + if @state == "done" and @url + open_url(@url) + elseif @state == "failed" + self\finish("options") + + retry: => + self\startUpload! if @state == "failed" + + finish: (state) => + return if @closing + @closing = true + self\hide! + @callback(state) + + draw: => + window_w, window_h = mp.get_osd_size() + ass = assdraw.ass_new() + ass\new_event() + self\setup_text(ass) + if @state == "uploading" + ass\append("Uploading (#{bold("#{@percent}%")})\\N") + ass\append("#{@filename}\\N") + ass\append("#{@sizeText} to #{@host.label}\\N\\N") + ass\append("#{bold('ESC:')} cancel upload\\N") + elseif @state == "done" + ass\append("#{bold('Upload complete')}\\N\\N") + ass\append("#{@url}\\N") + ass\append("Link copied to clipboard.\\N\\N") + ass\append("#{bold('c:')} copy link again\\N") + ass\append("#{bold('o:')} open in browser\\N") + ass\append("#{bold('ESC:')} close\\N") + elseif @state == "failed" + ass\append("#{bold('Upload failed')}\\N\\N") + ass\append("#{@errorMessage}\\N") + ass\append("The clip is still saved locally.\\N\\N") + ass\append("#{bold('r:')} retry upload\\N") + ass\append("#{bold('o:')} change upload options\\N") + ass\append("#{bold('ESC:')} close\\N") + mp.set_osd_ass(window_w, window_h, ass.text) diff --git a/src/encode.moon b/src/encode.moon index 4969511..be93e70 100644 --- a/src/encode.moon +++ b/src/encode.moon @@ -290,9 +290,10 @@ check_encoder = -> emit_event("encode-finished", "fail", explanation) return false -encode = (region, startTime, endTime) -> +encode = (region, startTime, endTime, onDone) -> format = formats[options.output_format] if not check_encoder! + onDone(false) if onDone return originalStartTime = startTime @@ -300,6 +301,7 @@ encode = (region, startTime, endTime) -> path, is_stream, is_temporary, startTime, endTime = find_path(startTime, endTime) if not path message("No file is being played") + onDone(false) if onDone return command = { @@ -419,6 +421,7 @@ encode = (region, startTime, endTime) -> if not res message("First pass failed! Check the logs for details.") emit_event("encode-finished", "fail") + onDone(false) if onDone return @@ -437,7 +440,7 @@ encode = (region, startTime, endTime) -> msg.info("Encoding to", out_path) msg.verbose("Command line:", table.concat(command, " ")) - if options.run_detached + if options.run_detached and not onDone message("Started encode, process was detached.") utils.subprocess_detached({args: command}) else @@ -453,9 +456,11 @@ encode = (region, startTime, endTime) -> emit_event("encode-finished", "success") if options.completion_command != "" mp.command(options.completion_command\gsub("%%{output}", out_path)) + onDone(true, out_path) if onDone else message("Encode failed! Check the logs for details.") emit_event("encode-finished", "fail") + onDone(false) if onDone -- Clean up pass log file. diff --git a/src/options.lua b/src/options.lua index 2ad1047..dceca5e 100644 --- a/src/options.lua +++ b/src/options.lua @@ -89,6 +89,19 @@ local options = { -- MPV command to run upon successful encoding -- %{output} will be replaced with the path to the resulting file. completion_command = "", + -- Where "encode & upload" (u) sends the clip. Only the Catbox services are + -- supported: "catbox" (permanent until 2 years of inactivity) or + -- "litterbox" (temporary, expires after litterbox_time). + upload_host = "catbox", + -- Expiry for litterbox uploads: 1h, 12h, 24h or 72h. + litterbox_time = "24h", + -- Optional Catbox account hash. Account uploads are permanent and can be + -- managed (deleted) from the Catbox account. + catbox_userhash = "", + -- curl executable used for the multipart upload. + upload_curl_path = "curl", + -- Open the uploaded link in the browser as soon as it is ready. + open_after_upload = false, } mpopts.read_options(options) diff --git a/src/testing.moon b/src/testing.moon index 40cc21b..d7ba4c3 100644 --- a/src/testing.moon +++ b/src/testing.moon @@ -23,6 +23,7 @@ register_test_handlers = (main_page) -> emit_event("range-set") ) mp.register_script_message("mpv-webm-encode", -> main_page\encode!) + mp.register_script_message("mpv-webm-upload", -> main_page\upload!) mp.register_script_message("mpv-webm-get-state", -> mouse_x, mouse_y = mp.get_mouse_pos! osd_w, osd_h = mp.get_osd_size! diff --git a/src/upload.moon b/src/upload.moon new file mode 100644 index 0000000..b785b28 --- /dev/null +++ b/src/upload.moon @@ -0,0 +1,100 @@ +-- Uploading the encoded clip. Only Catbox's two services are supported: both +-- expose the same anonymous multipart API, differing only by endpoint and the +-- required litterbox expiry. streamable.com is intentionally absent: its upload +-- API requires an account (HTTP 401) and its web upload routes 404. +-- +-- The POST itself is a single curl call. Asynchronous subprocess runs let the +-- progress page stay responsive while the upload is in flight. + +hosts = + catbox: + id: "catbox" + label: "catbox.moe" + url: "https://catbox.moe/user/api.php" + time: nil + litterbox: + id: "litterbox" + label: "litterbox.catbox.moe" + url: "https://litterbox.catbox.moe/resources/internals/api.php" + time: () -> options.litterbox_time + +get_upload_host = (id) -> + return hosts[id] or hosts.catbox + +upload_host_possible_values = () -> + {{"catbox", "catbox.moe"}, {"litterbox", "litterbox.catbox.moe"}} + +-- curl is the only external tool uploads need. Probe it once and cache. +upload_available = nil +is_upload_available = () -> + if upload_available == nil + res = utils.subprocess({args: {options.upload_curl_path, "--version"}, playback_only: false}) + upload_available = res != nil and res.status == 0 + return upload_available + +-- curl treats comma and semicolon specially inside -F values; escape them so a +-- filename containing either still resolves to a single file. +escape_form_path = (path) -> + path\gsub("([,;])", "\\%1") + +-- The response body (the URL) goes to response_path; curl's progress meter and +-- errors go to progress_path; the HTTP status is printed to stdout. +build_upload_args = (path, host_id, response_path, progress_path) -> + host = get_upload_host(host_id) + args = {options.upload_curl_path, "-#", "-S", "-o", response_path, + "--stderr", progress_path, "-w", "%{http_code}"} + append(args, {"-F", "reqtype=fileupload"}) + + time = host.time + time = time() if type(time) == "function" + if time and time != "" + append(args, {"-F", "time=#{time}"}) + + if host.id == "catbox" and options.catbox_userhash != "" + append(args, {"-F", "userhash=#{options.catbox_userhash}"}) + + append(args, {"-F", "fileToUpload=@#{escape_form_path(path)}"}) + append(args, {host.url}) + return args + +read_file = (path) -> + file = io.open(path, "r") + return nil if not file + content = file\read("*a") + file\close! + return content + +is_macos = file_exists("/Applications") and not is_windows + +-- Prefer mpv's own clipboard property; fall back to the platform tool when the +-- running mpv is too old to accept writes to clipboard/text. Those tools keep +-- running to own the selection, so they are launched without waiting. +copy_to_clipboard = (text) -> + ok, written = pcall(() -> mp.set_property("clipboard/text", text)) + return true if ok and written + + candidates = {} + if is_windows + candidates = {{"clip"}} + elseif is_macos + candidates = {{"pbcopy"}} + else + candidates = {{"wl-copy"}, {"xclip", "-selection", "clipboard"}, {"xsel", "--clipboard", "--input"}} + + for args in *candidates + handle = mp.command_native_async({ + name: "subprocess" + args: args + stdin_data: text + playback_only: false + }) + return true if handle + return false + +open_url = (url) -> + if is_windows + utils.subprocess_detached({args: {"cmd", "/c", "start", "", url}}) + elseif is_macos + utils.subprocess_detached({args: {"open", url}}) + else + utils.subprocess_detached({args: {"xdg-open", url}}) diff --git a/tests/testcases/test_upload.py b/tests/testcases/test_upload.py new file mode 100644 index 0000000..5426fdc --- /dev/null +++ b/tests/testcases/test_upload.py @@ -0,0 +1,99 @@ +"""Integration coverage for the "encode & upload" flow (issue #228). + +The network is replaced by a stub curl, so these tests exercise the real script +path — keybinding handler, encoder, async subprocess, progress/done states — +without depending on Catbox being reachable. +""" + +import stat + +from .base_test_case import BaseTestCase + + +# Handles --version (the availability probe) and the real invocation: writes the +# URL to the -o target, a progress meter to the --stderr target, and 200 to +# stdout. Records its arguments so the test can assert the request shape. +STUB_CURL = r"""#!/bin/sh +if [ "$1" = "--version" ]; then + echo "curl 8.0.0" + exit 0 +fi +printf '%s\n' "$@" > "$(dirname "$0")/stub-args.txt" +out="" +err="" +while [ $# -gt 0 ]; do + case "$1" in + -o) out="$2"; shift 2;; + --stderr) err="$2"; shift 2;; + *) shift;; + esac +done +[ -n "$out" ] && printf 'https://files.catbox.moe/stub123.webm\n' > "$out" +[ -n "$err" ] && printf '#### 100.0%%\n' > "$err" +printf '200' +exit 0 +""" + + +class TestUpload(BaseTestCase): + def makeStubCurl(self): + stub = self.tempdir / "stub-curl" + stub.write_text(STUB_CURL) + stub.chmod(stub.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) + return stub + + def upload(self): + event = self.scriptMessage("mpv-webm-upload", event="webm-upload-finished", timeout=60) + self.assertEqual(event.args, ["webm-upload-finished", "done"], self.getLog()) + return (self.tempdir / "stub-args.txt").read_text() + + def closeUploadPage(self): + self.sendKeyPress("ESC") + self.waitUntil(lambda: self.getState()["mainVisible"], "main page after closing upload") + + def test_encode_and_upload_both_hosts(self): + stub = self.makeStubCurl() + self.openTestVideoFile(self.createVideo(size="320x180", duration=3)) + + cases = ( + ("catbox", "24h", ["https://catbox.moe/user/api.php"], ["time=", "userhash="]), + ("litterbox", "72h", + ["https://litterbox.catbox.moe/resources/internals/api.php", "time=72h"], ["userhash="]), + ) + for host, litterbox_time, present, absent in cases: + with self.subTest(host=host): + self.updateScriptOptions({ + "output_format": "avc", + "output_template": "clip", + "display_progress": False, + "run_detached": False, + "upload_host": host, + "litterbox_time": litterbox_time, + "upload_curl_path": str(stub), + }) + self.setRange(1, 2) + args = self.upload() + + for token in present: + self.assertIn(token, args) + for token in absent: + self.assertNotIn(token, args) + self.assertIn("reqtype=fileupload", args) + self.assertIn("fileToUpload=@", args) + self.assertTrue((self.tempdir / "clip.mp4").exists()) + + self.closeUploadPage() + + def test_upload_aborts_without_times(self): + self.makeStubCurl() + self.openTestVideoFile(self.createVideo(size="320x180", duration=3)) + self.updateScriptOptions({ + "output_format": "avc", + "output_template": "clip", + "upload_curl_path": str(self.tempdir / "stub-curl"), + }) + self.setRange(-1, -1) + # No encode should start, so no upload-finished event should arrive. + self.scriptMessage("mpv-webm-upload") + self.assertIsNone(self.mpv_ipc.wait_for_event("webm-upload-finished", 1), self.getLog()) + self.assertFalse((self.tempdir / "stub-args.txt").exists()) From 63027d1ff57fc01fb8a36c5af432821d688e6d2d Mon Sep 17 00:00:00 2001 From: ekisu Date: Sun, 13 Sep 2026 23:00:52 -0300 Subject: [PATCH 2/2] Handle older mpv clipboard fallback; split upload tests - command_native_async requires a callback on older mpv (0.33 calls it unconditionally), so pass a no-op instead of nil. - Set the done state before copying/opening so a clipboard or browser failure cannot suppress the result. - Run the catbox and litterbox cases as separate tests instead of reopening pages via an IPC keypress, which older mpv handles inconsistently in the harness. --- src/UploadWithProgress.moon | 4 +- src/upload.moon | 2 +- tests/testcases/test_upload.py | 75 +++++++++++++++------------------- 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/src/UploadWithProgress.moon b/src/UploadWithProgress.moon index a1b9dea..ff04a9e 100644 --- a/src/UploadWithProgress.moon +++ b/src/UploadWithProgress.moon @@ -85,9 +85,9 @@ class UploadWithProgress extends Page if uploadOk @url = response - copy_to_clipboard(@url) - open_url(@url) if options.open_after_upload @state = "done" + pcall(() -> copy_to_clipboard(@url)) + pcall(() -> open_url(@url)) if options.open_after_upload else @state = "failed" @errorMessage = response diff --git a/src/upload.moon b/src/upload.moon index b785b28..1dd4ee2 100644 --- a/src/upload.moon +++ b/src/upload.moon @@ -87,7 +87,7 @@ copy_to_clipboard = (text) -> args: args stdin_data: text playback_only: false - }) + }, (-> nil)) return true if handle return false diff --git a/tests/testcases/test_upload.py b/tests/testcases/test_upload.py index 5426fdc..d22e84d 100644 --- a/tests/testcases/test_upload.py +++ b/tests/testcases/test_upload.py @@ -36,61 +36,50 @@ class TestUpload(BaseTestCase): - def makeStubCurl(self): - stub = self.tempdir / "stub-curl" - stub.write_text(STUB_CURL) - stub.chmod(stub.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) - return stub + def setUp(self): + super().setUp() + self.stub = self.tempdir / "stub-curl" + self.stub.write_text(STUB_CURL) + self.stub.chmod(self.stub.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) - def upload(self): + def encodeAndUpload(self, **options): + self.openTestVideoFile(self.createVideo(size="320x180", duration=3)) + settings = { + "output_format": "avc", + "output_template": "clip", + "display_progress": False, + "run_detached": False, + "upload_curl_path": str(self.stub), + } + settings.update(options) + self.updateScriptOptions(settings) + self.setRange(1, 2) event = self.scriptMessage("mpv-webm-upload", event="webm-upload-finished", timeout=60) self.assertEqual(event.args, ["webm-upload-finished", "done"], self.getLog()) + self.assertTrue((self.tempdir / "clip.mp4").exists()) return (self.tempdir / "stub-args.txt").read_text() - def closeUploadPage(self): - self.sendKeyPress("ESC") - self.waitUntil(lambda: self.getState()["mainVisible"], "main page after closing upload") - - def test_encode_and_upload_both_hosts(self): - stub = self.makeStubCurl() - self.openTestVideoFile(self.createVideo(size="320x180", duration=3)) - - cases = ( - ("catbox", "24h", ["https://catbox.moe/user/api.php"], ["time=", "userhash="]), - ("litterbox", "72h", - ["https://litterbox.catbox.moe/resources/internals/api.php", "time=72h"], ["userhash="]), - ) - for host, litterbox_time, present, absent in cases: - with self.subTest(host=host): - self.updateScriptOptions({ - "output_format": "avc", - "output_template": "clip", - "display_progress": False, - "run_detached": False, - "upload_host": host, - "litterbox_time": litterbox_time, - "upload_curl_path": str(stub), - }) - self.setRange(1, 2) - args = self.upload() - - for token in present: - self.assertIn(token, args) - for token in absent: - self.assertNotIn(token, args) - self.assertIn("reqtype=fileupload", args) - self.assertIn("fileToUpload=@", args) - self.assertTrue((self.tempdir / "clip.mp4").exists()) + def test_catbox_upload(self): + args = self.encodeAndUpload(upload_host="catbox") + self.assertIn("https://catbox.moe/user/api.php", args) + self.assertIn("reqtype=fileupload", args) + self.assertIn("fileToUpload=@", args) + # catbox is permanent and optional account-based: no expiry, no hash here. + self.assertNotIn("time=", args) + self.assertNotIn("userhash=", args) - self.closeUploadPage() + def test_litterbox_upload_sends_expiry(self): + args = self.encodeAndUpload(upload_host="litterbox", litterbox_time="72h") + self.assertIn("https://litterbox.catbox.moe/resources/internals/api.php", args) + self.assertIn("time=72h", args) + self.assertNotIn("userhash=", args) def test_upload_aborts_without_times(self): - self.makeStubCurl() self.openTestVideoFile(self.createVideo(size="320x180", duration=3)) self.updateScriptOptions({ "output_format": "avc", "output_template": "clip", - "upload_curl_path": str(self.tempdir / "stub-curl"), + "upload_curl_path": str(self.stub), }) self.setRange(-1, -1) # No encode should start, so no upload-finished event should arrive.