mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 18:22:41 -08:00
Overlay 2.0 (#12)
This commit is contained in:
63
scripts/dev-watch.sh
Executable file
63
scripts/dev-watch.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
electron_args=("$@")
|
||||
if [[ ${#electron_args[@]} -eq 0 ]]; then
|
||||
electron_args=(--start --dev)
|
||||
fi
|
||||
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
echo "[ERROR] bun not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TS_WATCH_PID=""
|
||||
RENDER_WATCH_PID=""
|
||||
|
||||
cleanup() {
|
||||
local pids=("$TS_WATCH_PID" "$RENDER_WATCH_PID")
|
||||
for pid in "${pids[@]}"; do
|
||||
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
sync_renderer_assets() {
|
||||
mkdir -p dist/renderer
|
||||
cp src/renderer/index.html src/renderer/style.css dist/renderer/
|
||||
mkdir -p dist/renderer/fonts
|
||||
cp -R src/renderer/fonts/. dist/renderer/fonts/
|
||||
}
|
||||
|
||||
echo "[INFO] Syncing renderer static assets"
|
||||
sync_renderer_assets
|
||||
|
||||
echo "[INFO] Running initial compile"
|
||||
bun run tsc
|
||||
bun run build:renderer
|
||||
|
||||
echo "[INFO] Starting TypeScript watch"
|
||||
bun run tsc --watch --preserveWatchOutput &
|
||||
TS_WATCH_PID=$!
|
||||
|
||||
echo "[INFO] Starting renderer watch"
|
||||
bunx esbuild src/renderer/renderer.ts \
|
||||
--bundle \
|
||||
--platform=browser \
|
||||
--format=esm \
|
||||
--target=es2022 \
|
||||
--outfile=dist/renderer/renderer.js \
|
||||
--sourcemap \
|
||||
--watch &
|
||||
RENDER_WATCH_PID=$!
|
||||
|
||||
echo "[INFO] Launching Electron with args: ${electron_args[*]}"
|
||||
bun run electron . "${electron_args[@]}"
|
||||
@@ -33,7 +33,7 @@ interface CliOptions {
|
||||
function parseCliArgs(argv: string[]): CliOptions {
|
||||
const args = [...argv];
|
||||
let inputParts: string[] = [];
|
||||
let dictionaryPath = path.join(process.cwd(), 'vendor', 'jiten_freq_global');
|
||||
let dictionaryPath = path.join(process.cwd(), 'vendor', 'frequency-dictionary');
|
||||
let emitPretty = false;
|
||||
let emitDiagnostics = false;
|
||||
let mecabCommand: string | undefined;
|
||||
@@ -394,7 +394,7 @@ function printUsage(): void {
|
||||
--color-band-5 <#hex> Frequency band-5 color.
|
||||
--color-known <#hex> Known-word color (default: #a6da95).
|
||||
--color-n-plus-one <#hex> N+1 target color (default: #c6a0f6).
|
||||
--dictionary <path> Frequency dictionary root path (default: ./vendor/jiten_freq_global)
|
||||
--dictionary <path> Frequency dictionary root path (default: ./vendor/frequency-dictionary)
|
||||
--mecab-command <path> Optional MeCab binary path (default: mecab)
|
||||
--mecab-dictionary <path> Optional MeCab dictionary directory (default: system default)
|
||||
-h, --help Show usage.
|
||||
|
||||
@@ -11,30 +11,36 @@ Description:
|
||||
Generates two browser-friendly files next to the input file:
|
||||
- <name>.mp4 (H.264 + AAC, prefers NVIDIA GPU if available)
|
||||
- <name>.webm (AV1/VP9 + Opus, prefers NVIDIA GPU if available)
|
||||
- <name>.gif (palette-optimised, 15 fps)
|
||||
- <name>-poster.jpg (single frame for video poster fallback)
|
||||
- <name>.webp (animated, only when --webp is provided)
|
||||
|
||||
Options:
|
||||
-f, --force Overwrite existing output files
|
||||
-w, --webp Generate animated WebP preview
|
||||
|
||||
Encoding profile:
|
||||
- Crop: 1920x1080 at x=760 y=180
|
||||
- Crop: 1920x1080 at x=760 y=200
|
||||
- MP4: H.264 + AAC
|
||||
- WebM: AV1/VP9 + Opus at 30 fps
|
||||
USAGE
|
||||
}
|
||||
|
||||
force=0
|
||||
generate_webp=0
|
||||
input=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-f|--force)
|
||||
-f | --force)
|
||||
force=1
|
||||
;;
|
||||
-w | --webp)
|
||||
generate_webp=1
|
||||
;;
|
||||
-*)
|
||||
echo "Error: unknown option: $1" >&2
|
||||
usage
|
||||
@@ -73,7 +79,8 @@ base="${filename%.*}"
|
||||
|
||||
mp4_out="$dir/$base.mp4"
|
||||
webm_out="$dir/$base.webm"
|
||||
gif_out="$dir/$base.gif"
|
||||
webp_out="$dir/$base.webp"
|
||||
poster_out="$dir/$base-poster.jpg"
|
||||
|
||||
overwrite_flag="-n"
|
||||
if [[ "$force" -eq 1 ]]; then
|
||||
@@ -81,7 +88,11 @@ if [[ "$force" -eq 1 ]]; then
|
||||
fi
|
||||
|
||||
if [[ "$force" -eq 0 ]]; then
|
||||
for output in "$mp4_out" "$webm_out" "$gif_out"; do
|
||||
outputs=("$mp4_out" "$webm_out" "$poster_out")
|
||||
if [[ "$generate_webp" -eq 1 ]]; then
|
||||
outputs+=("$webp_out")
|
||||
fi
|
||||
for output in "${outputs[@]}"; do
|
||||
if [[ -e "$output" ]]; then
|
||||
echo "Error: output exists: $output (use --force to overwrite)" >&2
|
||||
exit 1
|
||||
@@ -94,9 +105,8 @@ has_encoder() {
|
||||
ffmpeg -hide_banner -encoders 2> /dev/null | grep -qE "[[:space:]]${encoder}[[:space:]]"
|
||||
}
|
||||
|
||||
crop_vf="crop=1920:1080:760:180"
|
||||
crop_vf="crop=1920:1080:760:205"
|
||||
webm_vf="${crop_vf},fps=30"
|
||||
gif_vf="${crop_vf},fps=15,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3"
|
||||
|
||||
echo "Generating MP4: $mp4_out"
|
||||
if has_encoder "h264_nvenc"; then
|
||||
@@ -157,12 +167,32 @@ else
|
||||
"$webm_out"
|
||||
fi
|
||||
|
||||
echo "Generating GIF: $gif_out"
|
||||
ffmpeg "$overwrite_flag" -i "$input" \
|
||||
-vf "$gif_vf" \
|
||||
"$gif_out"
|
||||
if [[ "$generate_webp" -eq 1 ]]; then
|
||||
if ! has_encoder "libwebp"; then
|
||||
echo "Error: encoder not found: libwebp" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Generating animated WebP: $webp_out"
|
||||
ffmpeg "$overwrite_flag" -i "$input" \
|
||||
-vf "${crop_vf},fps=24,scale=960:-1:flags=lanczos" \
|
||||
-c:v libwebp \
|
||||
-q:v 80 \
|
||||
-loop 0 \
|
||||
-an \
|
||||
"$webp_out"
|
||||
fi
|
||||
|
||||
echo "Generating poster: $poster_out"
|
||||
ffmpeg "$overwrite_flag" -ss 00:00:05 -i "$input" \
|
||||
-vf "$crop_vf" \
|
||||
-vframes 1 \
|
||||
-q:v 2 \
|
||||
"$poster_out"
|
||||
|
||||
echo "Done."
|
||||
echo "MP4: $mp4_out"
|
||||
echo "WebM: $webm_out"
|
||||
echo "GIF: $gif_out"
|
||||
if [[ "$generate_webp" -eq 1 ]]; then
|
||||
echo "WebP: $webp_out"
|
||||
fi
|
||||
echo "Poster: $poster_out"
|
||||
|
||||
8
scripts/subminer-dev.sh
Executable file
8
scripts/subminer-dev.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
exec bun run electron . "$@"
|
||||
128
scripts/test-plugin-process-start-retries.lua
Normal file
128
scripts/test-plugin-process-start-retries.lua
Normal file
@@ -0,0 +1,128 @@
|
||||
local function assert_true(condition, message)
|
||||
if condition then
|
||||
return
|
||||
end
|
||||
error(message)
|
||||
end
|
||||
|
||||
local function has_flag(call, flag)
|
||||
local args = call.args or {}
|
||||
for _, arg in ipairs(args) do
|
||||
if arg == flag then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function has_timeout(timeouts, target)
|
||||
for _, value in ipairs(timeouts) do
|
||||
if math.abs(value - target) < 0.0001 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local recorded = {
|
||||
async_calls = {},
|
||||
timeouts = {},
|
||||
logs = {},
|
||||
}
|
||||
|
||||
local start_attempts = 0
|
||||
|
||||
local mp = {}
|
||||
|
||||
function mp.command_native_async(command, callback)
|
||||
recorded.async_calls[#recorded.async_calls + 1] = command
|
||||
|
||||
local success = true
|
||||
local result = { status = 0, stdout = "", stderr = "" }
|
||||
local err = nil
|
||||
|
||||
if has_flag(command, "--start") then
|
||||
start_attempts = start_attempts + 1
|
||||
if start_attempts == 1 then
|
||||
success = false
|
||||
result = { status = 1, stdout = "", stderr = "startup-not-ready" }
|
||||
err = "startup-not-ready"
|
||||
end
|
||||
end
|
||||
|
||||
if callback then
|
||||
callback(success, result, err)
|
||||
end
|
||||
end
|
||||
|
||||
function mp.add_timeout(seconds, callback)
|
||||
recorded.timeouts[#recorded.timeouts + 1] = seconds
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
local process_module = dofile("plugin/subminer/process.lua")
|
||||
local process = process_module.create({
|
||||
mp = mp,
|
||||
opts = {
|
||||
backend = "x11",
|
||||
socket_path = "/tmp/subminer.sock",
|
||||
log_level = "debug",
|
||||
texthooker_enabled = true,
|
||||
texthooker_port = 5174,
|
||||
auto_start_visible_overlay = false,
|
||||
},
|
||||
state = {
|
||||
binary_path = "/tmp/subminer",
|
||||
overlay_running = false,
|
||||
texthooker_running = false,
|
||||
},
|
||||
binary = {
|
||||
ensure_binary_available = function()
|
||||
return true
|
||||
end,
|
||||
},
|
||||
environment = {
|
||||
detect_backend = function()
|
||||
return "x11"
|
||||
end,
|
||||
},
|
||||
options_helper = {
|
||||
coerce_bool = function(value, default_value)
|
||||
if value == true or value == "yes" or value == "true" then
|
||||
return true
|
||||
end
|
||||
if value == false or value == "no" or value == "false" then
|
||||
return false
|
||||
end
|
||||
return default_value
|
||||
end,
|
||||
},
|
||||
log = {
|
||||
subminer_log = function(_level, _scope, line)
|
||||
recorded.logs[#recorded.logs + 1] = line
|
||||
end,
|
||||
show_osd = function(_) end,
|
||||
normalize_log_level = function(value)
|
||||
return value or "info"
|
||||
end,
|
||||
},
|
||||
})
|
||||
|
||||
process.start_overlay()
|
||||
|
||||
assert_true(start_attempts == 2, "expected start overlay command retry after readiness failure")
|
||||
assert_true(not has_timeout(recorded.timeouts, 0.35), "fixed texthooker wait (0.35s) should be removed")
|
||||
assert_true(not has_timeout(recorded.timeouts, 0.6), "fixed startup visibility delay (0.6s) should be removed")
|
||||
|
||||
local retry_timeout_seen = false
|
||||
for _, timeout_seconds in ipairs(recorded.timeouts) do
|
||||
if timeout_seconds > 0 and timeout_seconds <= 0.25 then
|
||||
retry_timeout_seen = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert_true(retry_timeout_seen, "expected shorter bounded retry timeout")
|
||||
|
||||
print("plugin process retry regression tests: OK")
|
||||
@@ -3,9 +3,12 @@ local function run_plugin_scenario(config)
|
||||
|
||||
local recorded = {
|
||||
async_calls = {},
|
||||
sync_calls = {},
|
||||
script_messages = {},
|
||||
events = {},
|
||||
osd = {},
|
||||
logs = {},
|
||||
property_sets = {},
|
||||
}
|
||||
|
||||
local function make_mp_stub()
|
||||
@@ -15,6 +18,9 @@ local function run_plugin_scenario(config)
|
||||
if name == "platform" then
|
||||
return config.platform or "linux"
|
||||
end
|
||||
if name == "input-ipc-server" then
|
||||
return config.input_ipc_server or ""
|
||||
end
|
||||
if name == "filename/no-ext" then
|
||||
return config.filename_no_ext or ""
|
||||
end
|
||||
@@ -34,7 +40,12 @@ local function run_plugin_scenario(config)
|
||||
return config.chapter_list or {}
|
||||
end
|
||||
|
||||
function mp.get_script_directory()
|
||||
return "plugin/subminer"
|
||||
end
|
||||
|
||||
function mp.command_native(command)
|
||||
recorded.sync_calls[#recorded.sync_calls + 1] = command
|
||||
local args = command.args or {}
|
||||
if args[1] == "ps" then
|
||||
return {
|
||||
@@ -44,6 +55,13 @@ local function run_plugin_scenario(config)
|
||||
}
|
||||
end
|
||||
if args[1] == "curl" then
|
||||
local url = args[#args] or ""
|
||||
if type(url) == "string" and url:find("myanimelist", 1, true) then
|
||||
return { status = 0, stdout = config.mal_lookup_stdout or "{}", stderr = "" }
|
||||
end
|
||||
if type(url) == "string" and url:find("api.aniskip.com", 1, true) then
|
||||
return { status = 0, stdout = config.aniskip_stdout or "{}", stderr = "" }
|
||||
end
|
||||
return { status = 0, stdout = "{}", stderr = "" }
|
||||
end
|
||||
return { status = 0, stdout = "", stderr = "" }
|
||||
@@ -52,6 +70,22 @@ local function run_plugin_scenario(config)
|
||||
function mp.command_native_async(command, callback)
|
||||
recorded.async_calls[#recorded.async_calls + 1] = command
|
||||
if callback then
|
||||
local args = command.args or {}
|
||||
if args[1] == "ps" then
|
||||
callback(true, { status = 0, stdout = config.process_list or "", stderr = "" }, nil)
|
||||
return
|
||||
end
|
||||
if args[1] == "curl" then
|
||||
local url = args[#args] or ""
|
||||
if type(url) == "string" and url:find("myanimelist", 1, true) then
|
||||
callback(true, { status = 0, stdout = config.mal_lookup_stdout or "{}", stderr = "" }, nil)
|
||||
return
|
||||
end
|
||||
if type(url) == "string" and url:find("api.aniskip.com", 1, true) then
|
||||
callback(true, { status = 0, stdout = config.aniskip_stdout or "{}", stderr = "" }, nil)
|
||||
return
|
||||
end
|
||||
end
|
||||
callback(true, { status = 0, stdout = "", stderr = "" }, nil)
|
||||
end
|
||||
end
|
||||
@@ -67,17 +101,28 @@ local function run_plugin_scenario(config)
|
||||
end
|
||||
|
||||
function mp.add_key_binding(_keys, _name, _fn) end
|
||||
function mp.register_event(_name, _fn) end
|
||||
function mp.register_event(name, fn)
|
||||
if recorded.events[name] == nil then
|
||||
recorded.events[name] = {}
|
||||
end
|
||||
recorded.events[name][#recorded.events[name] + 1] = fn
|
||||
end
|
||||
function mp.add_hook(_name, _prio, _fn) end
|
||||
function mp.observe_property(_name, _kind, _fn) end
|
||||
function mp.osd_message(message, _duration)
|
||||
recorded.osd[#recorded.osd + 1] = message
|
||||
end
|
||||
function mp.set_osd_ass(...) end
|
||||
function mp.get_time()
|
||||
return 0
|
||||
end
|
||||
function mp.commandv(...) end
|
||||
function mp.set_property_native(...) end
|
||||
function mp.set_property_native(name, value)
|
||||
recorded.property_sets[#recorded.property_sets + 1] = {
|
||||
name = name,
|
||||
value = value,
|
||||
}
|
||||
end
|
||||
function mp.get_script_name()
|
||||
return "subminer"
|
||||
end
|
||||
@@ -90,8 +135,8 @@ local function run_plugin_scenario(config)
|
||||
local utils = {}
|
||||
|
||||
function options.read_options(target, _name)
|
||||
if config.socket_path then
|
||||
target.socket_path = config.socket_path
|
||||
for key, value in pairs(config.option_overrides or {}) do
|
||||
target[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -108,7 +153,35 @@ local function run_plugin_scenario(config)
|
||||
return table.concat(parts, "/")
|
||||
end
|
||||
|
||||
function utils.parse_json(_json)
|
||||
function utils.parse_json(json)
|
||||
if json == "__MAL_FOUND__" then
|
||||
return {
|
||||
categories = {
|
||||
{
|
||||
items = {
|
||||
{
|
||||
id = 99,
|
||||
name = "Sample Show",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
end
|
||||
if json == "__ANISKIP_FOUND__" then
|
||||
return {
|
||||
found = true,
|
||||
results = {
|
||||
{
|
||||
skip_type = "op",
|
||||
interval = {
|
||||
start_time = 12.3,
|
||||
end_time = 45.6,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
end
|
||||
return {}, nil
|
||||
end
|
||||
|
||||
@@ -149,7 +222,7 @@ local function run_plugin_scenario(config)
|
||||
return utils
|
||||
end
|
||||
|
||||
local ok, err = pcall(dofile, "plugin/subminer.lua")
|
||||
local ok, err = pcall(dofile, "plugin/subminer/main.lua")
|
||||
if not ok then
|
||||
return nil, err, recorded
|
||||
end
|
||||
@@ -168,6 +241,82 @@ local function find_start_call(async_calls)
|
||||
local args = call.args or {}
|
||||
for i = 1, #args do
|
||||
if args[i] == "--start" then
|
||||
return call
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function count_start_calls(async_calls)
|
||||
local count = 0
|
||||
for _, call in ipairs(async_calls) do
|
||||
local args = call.args or {}
|
||||
for _, value in ipairs(args) do
|
||||
if value == "--start" then
|
||||
count = count + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local function find_control_call(async_calls, flag)
|
||||
for _, call in ipairs(async_calls) do
|
||||
local args = call.args or {}
|
||||
local has_flag = false
|
||||
local has_start = false
|
||||
for _, value in ipairs(args) do
|
||||
if value == flag then
|
||||
has_flag = true
|
||||
elseif value == "--start" then
|
||||
has_start = true
|
||||
end
|
||||
end
|
||||
if has_flag and not has_start then
|
||||
return call
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function call_has_arg(call, target)
|
||||
local args = (call and call.args) or {}
|
||||
for _, value in ipairs(args) do
|
||||
if value == target then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function has_sync_command(sync_calls, executable)
|
||||
for _, call in ipairs(sync_calls) do
|
||||
local args = call.args or {}
|
||||
if args[1] == executable then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function has_async_command(async_calls, executable)
|
||||
for _, call in ipairs(async_calls) do
|
||||
local args = call.args or {}
|
||||
if args[1] == executable then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function has_async_curl_for(async_calls, needle)
|
||||
for _, call in ipairs(async_calls) do
|
||||
local args = call.args or {}
|
||||
if args[1] == "curl" then
|
||||
local url = args[#args] or ""
|
||||
if type(url) == "string" and url:find(needle, 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -175,11 +324,50 @@ local function find_start_call(async_calls)
|
||||
return false
|
||||
end
|
||||
|
||||
local function has_property_set(property_sets, name, value)
|
||||
for _, call in ipairs(property_sets) do
|
||||
if call.name == name and call.value == value then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function has_osd_message(messages, target)
|
||||
for _, message in ipairs(messages) do
|
||||
if message == target then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function count_osd_message(messages, target)
|
||||
local count = 0
|
||||
for _, message in ipairs(messages) do
|
||||
if message == target then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local function fire_event(recorded, name)
|
||||
local listeners = recorded.events[name] or {}
|
||||
for _, listener in ipairs(listeners) do
|
||||
listener()
|
||||
end
|
||||
end
|
||||
|
||||
local binary_path = "/tmp/subminer-binary"
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "no",
|
||||
},
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
@@ -187,7 +375,227 @@ do
|
||||
assert_true(recorded ~= nil, "plugin failed to load for cold-start scenario: " .. tostring(err))
|
||||
assert_true(recorded.script_messages["subminer-start"] ~= nil, "subminer-start script message not registered")
|
||||
recorded.script_messages["subminer-start"]("texthooker=no")
|
||||
assert_true(find_start_call(recorded.async_calls), "expected cold-start to invoke --start command when process is absent")
|
||||
assert_true(find_start_call(recorded.async_calls) ~= nil, "expected cold-start to invoke --start command when process is absent")
|
||||
assert_true(
|
||||
not has_sync_command(recorded.sync_calls, "ps"),
|
||||
"expected cold-start start command to avoid synchronous process list scan"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "no",
|
||||
},
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for non-subminer file-load scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(not has_sync_command(recorded.sync_calls, "ps"), "file-loaded should avoid synchronous process checks")
|
||||
assert_true(not has_sync_command(recorded.sync_calls, "curl"), "file-loaded should avoid synchronous AniSkip network calls")
|
||||
assert_true(
|
||||
not has_async_curl_for(recorded.async_calls, "myanimelist.net/search/prefix.json"),
|
||||
"file-loaded without SubMiner context should skip AniSkip MAL lookup"
|
||||
)
|
||||
assert_true(
|
||||
not has_async_curl_for(recorded.async_calls, "api.aniskip.com"),
|
||||
"file-loaded without SubMiner context should skip AniSkip API lookup"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "no",
|
||||
},
|
||||
media_title = "Sample Show S01E01",
|
||||
mal_lookup_stdout = "__MAL_FOUND__",
|
||||
aniskip_stdout = "__ANISKIP_FOUND__",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for script-message AniSkip scenario: " .. tostring(err))
|
||||
assert_true(recorded.script_messages["subminer-aniskip-refresh"] ~= nil, "subminer-aniskip-refresh script message not registered")
|
||||
recorded.script_messages["subminer-aniskip-refresh"]()
|
||||
assert_true(not has_sync_command(recorded.sync_calls, "curl"), "AniSkip refresh should not perform synchronous curl calls")
|
||||
assert_true(has_async_command(recorded.async_calls, "curl"), "AniSkip refresh should perform async curl calls")
|
||||
assert_true(
|
||||
has_async_curl_for(recorded.async_calls, "myanimelist.net/search/prefix.json"),
|
||||
"AniSkip refresh should perform MAL lookup even when app is not running"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
auto_start_pause_until_ready = "no",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for visible auto-start scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
local start_call = find_start_call(recorded.async_calls)
|
||||
assert_true(start_call ~= nil, "auto-start should issue --start command")
|
||||
assert_true(
|
||||
not call_has_arg(start_call, "--show-visible-overlay"),
|
||||
"auto-start should keep --start command free of --show-visible-overlay"
|
||||
)
|
||||
assert_true(
|
||||
not call_has_arg(start_call, "--hide-visible-overlay"),
|
||||
"auto-start should keep --start command free of --hide-visible-overlay"
|
||||
)
|
||||
assert_true(
|
||||
find_control_call(recorded.async_calls, "--show-visible-overlay") ~= nil,
|
||||
"auto-start with visible overlay enabled should issue a separate --show-visible-overlay command"
|
||||
)
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "pause", true),
|
||||
"auto-start visible overlay should not force pause without explicit pause-until-ready option"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for duplicate auto-start scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
count_start_calls(recorded.async_calls) == 1,
|
||||
"duplicate file-loaded events should not issue duplicate --start commands while overlay is already running"
|
||||
)
|
||||
assert_true(
|
||||
count_osd_message(recorded.osd, "SubMiner: Already running") == 0,
|
||||
"duplicate auto-start events should not show Already running OSD"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
auto_start_pause_until_ready = "yes",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for pause-until-ready scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
has_property_set(recorded.property_sets, "pause", true),
|
||||
"pause-until-ready auto-start should pause mpv before overlay ready"
|
||||
)
|
||||
assert_true(recorded.script_messages["subminer-autoplay-ready"] ~= nil, "subminer-autoplay-ready script message not registered")
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
has_property_set(recorded.property_sets, "pause", false),
|
||||
"autoplay-ready script message should resume mpv playback"
|
||||
)
|
||||
assert_true(
|
||||
has_osd_message(recorded.osd, "SubMiner: Loading subtitle annotations..."),
|
||||
"pause-until-ready auto-start should show loading OSD message"
|
||||
)
|
||||
assert_true(
|
||||
has_osd_message(recorded.osd, "SubMiner: Subtitle annotations loaded"),
|
||||
"autoplay-ready should show loaded OSD message"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "no",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for hidden auto-start scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
local start_call = find_start_call(recorded.async_calls)
|
||||
assert_true(start_call ~= nil, "auto-start should issue --start command")
|
||||
assert_true(
|
||||
not call_has_arg(start_call, "--hide-visible-overlay"),
|
||||
"auto-start should keep --start command free of --hide-visible-overlay"
|
||||
)
|
||||
assert_true(
|
||||
not call_has_arg(start_call, "--show-visible-overlay"),
|
||||
"auto-start should keep --start command free of --show-visible-overlay"
|
||||
)
|
||||
assert_true(
|
||||
find_control_call(recorded.async_calls, "--hide-visible-overlay") ~= nil,
|
||||
"auto-start with visible overlay disabled should issue a separate --hide-visible-overlay command"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = binary_path,
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "/tmp/other.sock",
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for mismatched socket auto-start scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
local start_call = find_start_call(recorded.async_calls)
|
||||
assert_true(
|
||||
start_call == nil,
|
||||
"auto-start should be skipped when mpv input-ipc-server does not match configured socket_path"
|
||||
)
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "pause", true),
|
||||
"pause-until-ready gate should not arm when socket_path does not match"
|
||||
)
|
||||
end
|
||||
|
||||
print("plugin start gate regression tests: OK")
|
||||
|
||||
Reference in New Issue
Block a user