feat(launcher): migrate aniskip resolution to launcher script opts

This commit is contained in:
2026-03-03 00:37:50 -08:00
parent 10ef535f9a
commit 092c56f98f
10 changed files with 712 additions and 7 deletions

View File

@@ -53,6 +53,9 @@ aniskip_mal_id=
# Force episode number (optional). Leave blank for filename/title detection.
aniskip_episode=
# Optional pre-fetched AniSkip payload for this media (JSON or base64 JSON). When set, the plugin uses this directly and skips network lookup.
aniskip_payload=
# Show intro skip OSD button while inside OP range.
aniskip_show_button=yes

View File

@@ -13,6 +13,12 @@ function M.create(ctx)
local mal_lookup_cache = {}
local payload_cache = {}
local title_context_cache = {}
local base64_reverse = {}
local base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for i = 1, #base64_chars do
base64_reverse[base64_chars:sub(i, i)] = i - 1
end
local function url_encode(text)
if type(text) ~= "string" then
@@ -25,6 +31,109 @@ function M.create(ctx)
return encoded:gsub(" ", "%%20")
end
local function parse_json_payload(text)
if type(text) ~= "string" then
return nil
end
local parsed, parse_error = utils.parse_json(text)
if type(parsed) == "table" then
return parsed
end
return nil, parse_error
end
local function decode_base64(input)
if type(input) ~= "string" then
return nil
end
local cleaned = input:gsub("%s", ""):gsub("-", "+"):gsub("_", "/")
cleaned = cleaned:match("^%s*(.-)%s*$") or ""
if cleaned == "" then
return nil
end
if #cleaned % 4 == 1 then
return nil
end
if #cleaned % 4 ~= 0 then
cleaned = cleaned .. string.rep("=", 4 - (#cleaned % 4))
end
if not cleaned:match("^[A-Za-z0-9+/%=]+$") then
return nil
end
local out = {}
local out_len = 0
for index = 1, #cleaned, 4 do
local c1 = cleaned:sub(index, index)
local c2 = cleaned:sub(index + 1, index + 1)
local c3 = cleaned:sub(index + 2, index + 2)
local c4 = cleaned:sub(index + 3, index + 3)
local v1 = base64_reverse[c1]
local v2 = base64_reverse[c2]
if not v1 or not v2 then
return nil
end
local v3 = c3 == "=" and 0 or base64_reverse[c3]
local v4 = c4 == "=" and 0 or base64_reverse[c4]
if (c3 ~= "=" and not v3) or (c4 ~= "=" and not v4) then
return nil
end
local n = (((v1 * 64 + v2) * 64 + v3) * 64 + v4)
local b1 = math.floor(n / 65536)
local remaining = n % 65536
local b2 = math.floor(remaining / 256)
local b3 = remaining % 256
out_len = out_len + 1
out[out_len] = string.char(b1)
if c3 ~= "=" then
out_len = out_len + 1
out[out_len] = string.char(b2)
end
if c4 ~= "=" then
out_len = out_len + 1
out[out_len] = string.char(b3)
end
end
return table.concat(out)
end
local function resolve_launcher_payload()
local raw_payload = type(opts.aniskip_payload) == "string" and opts.aniskip_payload or ""
local trimmed = raw_payload:match("^%s*(.-)%s*$") or ""
if trimmed == "" then
return nil
end
local parsed, parse_error = parse_json_payload(trimmed)
if type(parsed) == "table" then
return parsed
end
local url_decoded = trimmed:gsub("%%(%x%x)", function(hex)
local value = tonumber(hex, 16)
if value then
return string.char(value)
end
return "%"
end)
if url_decoded ~= trimmed then
parsed, parse_error = parse_json_payload(url_decoded)
if type(parsed) == "table" then
return parsed
end
end
local b64_decoded = decode_base64(trimmed)
if type(b64_decoded) == "string" and b64_decoded ~= "" then
parsed, parse_error = parse_json_payload(b64_decoded)
if type(parsed) == "table" then
return parsed
end
end
subminer_log("warn", "aniskip", "Invalid launcher AniSkip payload: " .. tostring(parse_error or "unparseable"))
return nil
end
local function run_json_curl_async(url, callback)
mp.command_native_async({
name = "subprocess",
@@ -296,6 +405,8 @@ function M.create(ctx)
state.aniskip.episode = nil
state.aniskip.intro_start = nil
state.aniskip.intro_end = nil
state.aniskip.payload = nil
state.aniskip.payload_source = nil
remove_aniskip_chapters()
end
@@ -366,7 +477,17 @@ function M.create(ctx)
state.aniskip.intro_end = intro_end
state.aniskip.prompt_shown = false
set_intro_chapters(intro_start, intro_end)
subminer_log("info", "aniskip", string.format("Intro window %.3f -> %.3f (MAL %d, ep %d)", intro_start, intro_end, mal_id, episode))
subminer_log(
"info",
"aniskip",
string.format(
"Intro window %.3f -> %.3f (MAL %s, ep %s)",
intro_start,
intro_end,
tostring(mal_id or "-"),
tostring(episode or "-")
)
)
return true
end
end
@@ -374,6 +495,10 @@ function M.create(ctx)
return false
end
local function has_launcher_payload()
return type(opts.aniskip_payload) == "string" and opts.aniskip_payload:match("%S") ~= nil
end
local function is_launcher_context()
local forced_title = type(opts.aniskip_title) == "string" and (opts.aniskip_title:match("^%s*(.-)%s*$") or "") or ""
if forced_title ~= "" then
@@ -391,6 +516,9 @@ function M.create(ctx)
if forced_season and forced_season > 0 then
return true
end
if has_launcher_payload() then
return true
end
return false
end
@@ -500,6 +628,18 @@ function M.create(ctx)
end)
end
local function fetch_payload_from_launcher(payload, mal_id, title, episode)
if not payload then
return false
end
state.aniskip.payload = payload
state.aniskip.payload_source = "launcher"
state.aniskip.mal_id = mal_id
state.aniskip.title = title
state.aniskip.episode = episode
return apply_aniskip_payload(mal_id, title, episode, payload)
end
local function fetch_aniskip_for_current_media(trigger_source)
local trigger = type(trigger_source) == "string" and trigger_source or "manual"
if not opts.aniskip_enabled then
@@ -518,6 +658,28 @@ function M.create(ctx)
reset_aniskip_fields()
local title, episode, season = resolve_title_and_episode()
local lookup_titles = resolve_lookup_titles(title)
local launcher_payload = resolve_launcher_payload()
if launcher_payload then
local launcher_mal_id = tonumber(opts.aniskip_mal_id)
if not launcher_mal_id then
launcher_mal_id = nil
end
if fetch_payload_from_launcher(launcher_payload, launcher_mal_id, title, episode) then
subminer_log(
"info",
"aniskip",
string.format(
"Using launcher-provided AniSkip payload (title=%s, season=%s, episode=%s)",
tostring(title or ""),
tostring(season or "-"),
tostring(episode or "-")
)
)
return
end
subminer_log("info", "aniskip", "Launcher payload present but no OP interval was available")
return
end
subminer_log(
"info",
@@ -558,6 +720,8 @@ function M.create(ctx)
end
return
end
state.aniskip.payload = payload
state.aniskip.payload_source = "remote"
if not apply_aniskip_payload(mal_id, title, episode, payload) then
subminer_log("info", "aniskip", "AniSkip payload did not include OP interval")
end

View File

@@ -17,6 +17,7 @@ function M.load(options_lib, default_socket_path)
aniskip_season = "",
aniskip_mal_id = "",
aniskip_episode = "",
aniskip_payload = "",
aniskip_show_button = true,
aniskip_button_text = "You can skip by pressing %s",
aniskip_button_key = "y-k",

View File

@@ -24,6 +24,8 @@ function M.new()
episode = nil,
intro_start = nil,
intro_end = nil,
payload = nil,
payload_source = nil,
found = false,
prompt_shown = false,
},