diff --git a/.bash_aliases b/.bash_aliases index 3574dd6..8239ccf 100644 --- a/.bash_aliases +++ b/.bash_aliases @@ -131,3 +131,5 @@ alias impv='mpv --profile=subminer' alias smpv='mpv --profile=subminer' alias code='code --password-store=gnome-libsecret' +alias ccode='claude --dangerously-skip-permissions' +alias ccord='claude --channels plugin:discord@claude-plugins-official' diff --git a/.codex/AGENTS.md b/.codex/AGENTS.md index e81e433..5a5670a 100644 --- a/.codex/AGENTS.md +++ b/.codex/AGENTS.md @@ -14,8 +14,6 @@ Work style: telegraph; noun-phrases ok; drop grammar; min tokens. - Bugs: add regression test when it fits. - Keep files <~500 LOC; split/refactor as needed. - Commits: Conventional Commits (`feat|fix|refactor|build|ci|chore|docs|style|perf|test`). -- Subagents: read [Subagent Coordination Protocol](#subagent-coordination-protocol). -- If `Backlog.md` is set up for the project, each task must be associated with a ticket on the backlog. Create a new ticket on the board if it does not already exist - Editor: `code `. - CI: `gh run list/view` (rerun/fix til green). - Prefer end-to-end verify; if blocked, say what’s missing. diff --git a/.codex/config.toml##os.Linux b/.codex/config.toml##os.Linux index a075d0e..de3e3ba 100644 --- a/.codex/config.toml##os.Linux +++ b/.codex/config.toml##os.Linux @@ -1,4 +1,4 @@ -model = "gpt-5.4-mini" +model = "gpt-5.5" model_reasoning_effort = "high" personality = "pragmatic" tool_output_token_limit = 25000 @@ -24,6 +24,10 @@ js_repl = true [mcp_servers.deepwiki] url = "https://mcp.deepwiki.com/mcp" +[mcp_servers.anki] +command = "npx" +args = ["mcp-remote", "http://127.0.0.1:3141"] + [mcp_servers.backlog] command = "backlog" args = ["mcp", "start"] @@ -185,8 +189,29 @@ trust_level = "trusted" [projects."/home/sudacode/.config/rofi/scripts"] trust_level = "trusted" +[projects."/home/sudacode/github/SubMiner"] +trust_level = "trusted" + +[projects."/home/sudacode/github/SubMiner2"] +trust_level = "trusted" + +[projects."/home/sudacode/github/SubMiner-launchmode"] +trust_level = "trusted" + +[projects."/truenas/jellyfin/manga/raw/mangas/yotsubato"] +trust_level = "trusted" + +[projects."/home/sudacode/.local/share/Anki2/addons21/124672614"] +trust_level = "trusted" + +[projects."/home/sudacode/.local/share/Anki2"] +trust_level = "trusted" + [notice.model_migrations] "gpt-5.3-codex" = "gpt-5.4" [plugins."github@openai-curated"] enabled = true + +[tui.model_availability_nux] +"gpt-5.5" = 4 diff --git a/.config/SubMiner/config.jsonc##os.Linux b/.config/SubMiner/config.jsonc##os.Linux index 52478f2..454037a 100644 --- a/.config/SubMiner/config.jsonc##os.Linux +++ b/.config/SubMiner/config.jsonc##os.Linux @@ -147,20 +147,23 @@ "replace": true, }, "subtitleStyle": { - "fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP", - "fontSize": 35, + "fontFamily": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", + "fontSize": 38, "fontColor": "#cad3f5", - "fontWeight": 700, - "lineHeight": 1.35, - "letterSpacing": "-0.01em", + "fontWeight": 500, + "lineHeight": 1.4, + "letterSpacing": "0.02em", "wordSpacing": 0, "fontKerning": "normal", - "textRendering": "geometricPrecision", - "textShadow": "0 3px 10px rgba(0,0,0,0.69)", + "textRendering": "optimizeLegibility", + "paintOrder": "stroke fill", + "WebkitTextStroke": "1.5px #000", + "textShadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", "fontStyle": "normal", - "backgroundColor": "#232634", + "backgroundColor": "transparent", "hoverTokenColor": "#f4dbd6", - "hoverBackground": "rgba(54, 58, 79, 0.84)", + "backdropFilter": "blur(6px)", + "hoverTokenBackgroundColor": "transparent", "preserveLineBreaks": false, "autoPauseVideoOnHover": true, "autoPauseVideoOnYomitanPopup": true, @@ -168,6 +171,10 @@ "fontFamily": "Manrope, Inter", "fontSize": 24, "fontColor": "#cad3f5", + "backgroundColor": "transparent", + "paintOrder": "stroke fill", + "WebkitTextStroke": "0.75px #000", + "textShadow": "0 1px 4px rgba(0,0,0,0.85), 0 0 10px rgba(0,0,0,0.5)", }, "frequencyDictionary": { "enabled": true, @@ -176,14 +183,14 @@ "mode": "single", "matchMode": "headword", "singleColor": "#f5a97f", - "bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], + "bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#8bd5ca", "#8aadf4"], }, "enableJlpt": true, "jlptColors": { "N1": "#ed8796", "N2": "#f5a97f", "N3": "#f9e2af", - "N4": "#a6e3a1", + "N4": "#8bd5ca", "N5": "#8aadf4", }, "nPlusOneColor": "#c6a0f6", @@ -196,6 +203,7 @@ "maxEntryResults": 10, }, "anilist": { + "enabled": true, "characterDictionary": { "enabled": true, "collapsibleSections": { @@ -208,13 +216,6 @@ "immersionTracking": { "enabled": true, "dbPath": "", - "backend": { - "mode": "remote", - "remote": { - "baseUrl": "http://subminer-db:5432", - "deviceId": "cachypc", - }, - }, }, "jellyfin": { "enabled": true, @@ -283,7 +284,31 @@ "hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)", }, "controller": { - "preferredGamepadId": "8BitDo 8BitDo Ultimate 2 Wireless Controller for PC (Vendor: 2dc8 Product: 310b)", - "preferredGamepadLabel": "8BitDo 8BitDo Ultimate 2 Wireless Controller for PC (Vendor: 2dc8 Product: 310b)", + "enabled": true, + "profiles": { + "Sony Interactive Entertainment Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc)": { + "label": "Sony Interactive Entertainment Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc)", + "bindings": { + "toggleMpvPause": { + "kind": "button", + "buttonIndex": 10, + }, + "quitMpv": { + "kind": "button", + "buttonIndex": 8, + }, + }, + }, + }, + }, + "mpv": { + "executablePath": "", + "launchMode": "normal", + }, + "updates": { + "enabled": true, + "checkIntervalHours": 24, + "notificationType": "system", + "channel": "prerelease", }, } diff --git a/.config/btop/btop.conf b/.config/btop/btop.conf index 66b81e6..b0c1796 100644 --- a/.config/btop/btop.conf +++ b/.config/btop/btop.conf @@ -1,4 +1,4 @@ -#? Config file for btop v.1.4.6 +#? Config file for btop v.1.4.7 #* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. #* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" @@ -14,6 +14,11 @@ truecolor = true #* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. force_tty = false +#* Option to disable presets. Either the default preset, custom presets, or all presets. +#* "Off" All presets are enabled. +#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled. +disable_presets = "Off" + #* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. #* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. #* Use whitespace " " as separator between different presets. @@ -24,6 +29,9 @@ presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:defaul #* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. vim_keys = true +#* Disable all mouse events. +disable_mouse = false + #* Rounded corners on boxes, is ignored if TTY mode is ON. rounded_corners = true @@ -92,6 +100,9 @@ proc_left = false #* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). proc_filter_kernel = false +#* Should the process list follow the selected process when detailed view is open. +proc_follow_detailed = true + #* In tree-view, always accumulate child process resources in the parent process. proc_aggregate = false @@ -208,6 +219,9 @@ io_graph_combined = false #* Example: "/mnt/media:100 /:20 /boot:1". io_graph_speeds = "" +#* Swap the positions of the upload and download speed graphs. When true, upload will be on top. +swap_upload_download = false + #* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. net_download = 100 @@ -250,7 +264,7 @@ rsmi_measure_pcie_speeds = true #* Horizontally mirror the GPU graph. gpu_mirror_graph = true -#* Set which GPU vendors to show. Available values are "nvidia amd intel" +#* Set which GPU vendors to show. Available values are "nvidia amd intel apple" shown_gpus = "nvidia amd intel" #* Custom gpu0 model name, empty string to disable. diff --git a/.config/hypr/hyprland.conf b/.config/hypr/hyprland.conf index 036a5ca..48bf174 100644 --- a/.config/hypr/hyprland.conf +++ b/.config/hypr/hyprland.conf @@ -127,10 +127,10 @@ decoration { rounding_power = 2 # Change transparency of focused and unfocused windows - active_opacity = 0.88 - # active_opacity = 1.0 - inactive_opacity = 0.88 - # inactive_opacity = 1.0 + # active_opacity = 0.88 + active_opacity = 1.0 + # inactive_opacity = 0.88 + inactive_opacity = 1.0 shadow { enabled = true diff --git a/.config/hypr/windowrules.conf b/.config/hypr/windowrules.conf index c4244ad..20fc446 100644 --- a/.config/hypr/windowrules.conf +++ b/.config/hypr/windowrules.conf @@ -77,17 +77,18 @@ windowrule = border_size 0, match:title LunaTranslator windowrule = stay_focused on, match:class gsm_overlay # windowrule = fullscreen_state 2, match:class gsm_overlay -windowrule = float on, match:class subminer -windowrule = border_size 0, match:class subminer -windowrule = xray off override, match:class subminer -windowrule = no_shadow on, match:class subminer -windowrule = no_blur on, match:class subminer -windowrule = no_dim on, match:class subminer -windowrule = opaque on, match:class subminer -windowrule = dim_around off, match:class subminer -windowrule = allows_input offf, match:class subminer +windowrule = float on, match:class SubMiner +windowrule = border_size 0, match:class SubMiner +windowrule = xray off override, match:class SubMiner +windowrule = no_shadow on, match:class SubMiner +windowrule = no_blur on, match:class SubMiner +windowrule = no_dim on, match:class SubMiner +windowrule = opaque on, match:class SubMiner +windowrule = dim_around off, match:class SubMiner +windowrule = allows_input offf, match:class SubMiner windowrule = border_size 0, match:class steam_app_1277940 -windowrule = opacity 1.0 override, match:class subminer +windowrule = opacity 1.0 override, match:class SubMiner +windowrule = pin off, match:class SubMiner # }}} # {{{ FEH diff --git a/.config/mimeapps.list b/.config/mimeapps.list index 4b5027d..cedc8fc 100644 --- a/.config/mimeapps.list +++ b/.config/mimeapps.list @@ -70,6 +70,7 @@ x-scheme-handler/tg=org.telegram.desktop.desktop;org.telegram.desktop._f79d601e2 x-scheme-handler/tonsite=org.telegram.desktop.desktop; x-scheme-handler/tradingview=tradingview.desktop;TradingView.desktop; application/x-wine-extension-ini=nvim.desktop; +x-scheme-handler/subminer=subminer.desktop;SubMiner.desktop; [Default Applications] application/x-extension-htm=helium.desktop;zen.desktop @@ -157,3 +158,4 @@ x-scheme-handler/opencode=opencode-desktop-handler.desktop x-scheme-handler/subminer=subminer.desktop x-scheme-handler/claude-cli=claude-code-url-handler.desktop x-scheme-handler/mux=mux.desktop +x-scheme-handler/claude=com.anthropic.claude-desktop.desktop diff --git a/.config/mpv/mpv.conf##default b/.config/mpv/mpv.conf##default index 0ab77b9..153042f 100644 --- a/.config/mpv/mpv.conf##default +++ b/.config/mpv/mpv.conf##default @@ -7,7 +7,7 @@ scale=spline36 # Faster than ewa_lanczos for high-res video when shaders are off dither=fruit # Lighter dithering aimed at 8-bit or FRC panels # --- Window & interface --- -ontop=yes +ontop=no border=no no-border # autofit=50% # Start at half of the screen to avoid oversized windows on UHD displays @@ -201,3 +201,12 @@ sub-ass-override=strip sub-line-spacing=0.3 sub-hinting=light demuxer-mkv-subtitle-preroll=yes + +[youtube-cookies] +profile-desc="Apply YouTube cookies automatically" +profile-cond=path:find("youtu%.?be") +profile-restore=copy +cookies=yes +cookies-file=/truenas/sudacode/japanese/youtube-cookies.txt +ytdl-raw-options-append=cookies=/truenas/sudacode/japanese/youtube-cookies.txt + diff --git a/.config/mpv/mpv.conf##os.Darwin b/.config/mpv/mpv.conf##os.Darwin index b936c0a..01db8c0 100644 --- a/.config/mpv/mpv.conf##os.Darwin +++ b/.config/mpv/mpv.conf##os.Darwin @@ -33,6 +33,7 @@ sub-pos=90 ytdl-format=bestvideo+bestaudio/best ytdl-raw-options=sub-langs=en.*,write-auto-subs= ytdl-raw-options-append=extractor-args=youtubepot-bgutilhttp:base_url=http://tubearchivist:4416 + # Stats & UI colors (Catppuccin Macchiato) background-color='#24273a' osd-back-color='#181926' @@ -231,3 +232,12 @@ sub-italic=no sub-ass-override=strip sub-line-spacing=0.3 sub-hinting=light + +[youtube-cookies] +profile-desc="Apply YouTube cookies automatically" +profile-cond=path:find("youtu%.?be") +profile-restore=copy +cookies=yes +cookies-file=/Volumes/sudacode/japanese/youtube-cookies.txt +ytdl-raw-options-append=cookies=/Volumes/sudacode/japanese/youtube-cookies.txt + diff --git a/.config/mpv/mpv.conf##os.WSL b/.config/mpv/mpv.conf##os.WSL index dff4731..b309054 100644 --- a/.config/mpv/mpv.conf##os.WSL +++ b/.config/mpv/mpv.conf##os.WSL @@ -41,6 +41,15 @@ audio-wait-open=0.1 # --- Networking --- ytdl-format=bestvideo+bestaudio/best ytdl-raw-options=sub-langs=en.*,write-auto-subs= + +[youtube-cookies] +profile-desc="Apply YouTube cookies automatically" +profile-cond=path:find("youtu%.?be") +profile-restore=copy +cookies=yes +cookies-file="Z:/sudacode/japanese/cookies.Japanese.txt" +ytdl-raw-options-append=cookies=Z:/sudacode/japanese/cookies.Japanese.txt + # --- Video output & decoding --- vo=gpu-next hwdec=nvdec diff --git a/.config/mpv/scripts/subminer/aniskip.lua b/.config/mpv/scripts/subminer/aniskip.lua deleted file mode 100644 index dacf512..0000000 --- a/.config/mpv/scripts/subminer/aniskip.lua +++ /dev/null @@ -1,758 +0,0 @@ -local M = {} -local matcher = require("aniskip_match") -local DEFAULT_ANISKIP_BUTTON_KEY = "TAB" - -function M.create(ctx) - local mp = ctx.mp - local utils = ctx.utils - local opts = ctx.opts - local state = ctx.state - local environment = ctx.environment - local subminer_log = ctx.log.subminer_log - local show_osd = ctx.log.show_osd - local request_generation = 0 - 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 - return "" - end - local encoded = text:gsub("\n", " ") - encoded = encoded:gsub("([^%w%-_%.~ ])", function(char) - return string.format("%%%02X", string.byte(char)) - end) - return encoded:gsub(" ", "%%20") - end - - local function is_remote_media_path() - local media_path = mp.get_property("path") - if type(media_path) ~= "string" then - return false - end - local trimmed = media_path:match("^%s*(.-)%s*$") or "" - if trimmed == "" then - return false - end - return trimmed:match("^%a[%w+.-]*://") ~= nil - 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", - args = { "curl", "-sL", "--connect-timeout", "5", "-A", "SubMiner-mpv/ani-skip", url }, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }, function(success, result, error) - if not success or not result or result.status ~= 0 or type(result.stdout) ~= "string" or result.stdout == "" then - local detail = error or (result and result.stderr) or "curl failed" - callback(nil, detail) - return - end - local parsed, parse_error = utils.parse_json(result.stdout) - if type(parsed) ~= "table" then - callback(nil, parse_error or "invalid json") - return - end - callback(parsed, nil) - end) - end - - local function parse_episode_hint(text) - if type(text) ~= "string" or text == "" then - return nil - end - local patterns = { - "[Ss]%d+[Ee](%d+)", - "[Ee][Pp]?[%s%._%-]*(%d+)", - "[%s%._%-]+(%d+)[%s%._%-]+", - } - for _, pattern in ipairs(patterns) do - local token = text:match(pattern) - if token then - local episode = tonumber(token) - if episode and episode > 0 and episode < 10000 then - return episode - end - end - end - return nil - end - - local function cleanup_title(raw) - if type(raw) ~= "string" then - return nil - end - local cleaned = raw - cleaned = cleaned:gsub("%b[]", " ") - cleaned = cleaned:gsub("%b()", " ") - cleaned = cleaned:gsub("[Ss]%d+[Ee]%d+", " ") - cleaned = cleaned:gsub("[Ee][Pp]?[%s%._%-]*%d+", " ") - cleaned = cleaned:gsub("[%._%-]+", " ") - cleaned = cleaned:gsub("%s+", " ") - cleaned = cleaned:match("^%s*(.-)%s*$") or "" - if cleaned == "" then - return nil - end - return cleaned - end - - local function extract_show_title_from_path(media_path) - if type(media_path) ~= "string" or media_path == "" then - return nil - end - local normalized = media_path:gsub("\\", "/") - local segments = {} - for segment in normalized:gmatch("[^/]+") do - segments[#segments + 1] = segment - end - for index = 1, #segments do - local segment = segments[index] or "" - if segment:match("^[Ss]eason[%s%._%-]*%d+$") or segment:match("^[Ss][%s%._%-]*%d+$") then - local prior = segments[index - 1] - local cleaned = cleanup_title(prior or "") - if cleaned and cleaned ~= "" then - return cleaned - end - end - end - return nil - end - - local function resolve_title_and_episode() - local forced_title = type(opts.aniskip_title) == "string" and (opts.aniskip_title:match("^%s*(.-)%s*$") or "") or "" - local forced_season = tonumber(opts.aniskip_season) - local forced_episode = tonumber(opts.aniskip_episode) - local media_title = mp.get_property("media-title") - local filename = mp.get_property("filename/no-ext") or mp.get_property("filename") or "" - local path = mp.get_property("path") or "" - local cache_key = table.concat({ - tostring(forced_title or ""), - tostring(forced_season or ""), - tostring(forced_episode or ""), - tostring(media_title or ""), - tostring(filename or ""), - tostring(path or ""), - }, "\31") - local cached = title_context_cache[cache_key] - if type(cached) == "table" then - return cached.title, cached.episode, cached.season - end - local path_show_title = extract_show_title_from_path(path) - local candidate_title = nil - if path_show_title and path_show_title ~= "" then - candidate_title = path_show_title - elseif forced_title ~= "" then - candidate_title = forced_title - else - candidate_title = cleanup_title(media_title) or cleanup_title(filename) or cleanup_title(path) - end - local episode = forced_episode or parse_episode_hint(media_title) or parse_episode_hint(filename) or parse_episode_hint(path) or 1 - title_context_cache[cache_key] = { - title = candidate_title, - episode = episode, - season = forced_season, - } - return candidate_title, episode, forced_season - end - - local function select_best_mal_item(items, title, season) - if type(items) ~= "table" then - return nil - end - local best_item = nil - local best_score = -math.huge - for _, item in ipairs(items) do - if type(item) == "table" and tonumber(item.id) then - local candidate_name = tostring(item.name or "") - local score = matcher.title_overlap_score(title, candidate_name) + matcher.season_signal_score(season, candidate_name) - if score > best_score then - best_score = score - best_item = item - end - end - end - return best_item - end - - local function resolve_mal_id_async(title, season, request_id, callback) - local forced_mal_id = tonumber(opts.aniskip_mal_id) - if forced_mal_id and forced_mal_id > 0 then - callback(forced_mal_id, "(forced-mal-id)") - return - end - if type(title) == "string" and title:match("^%d+$") then - local numeric = tonumber(title) - if numeric and numeric > 0 then - callback(numeric, title) - return - end - end - if type(title) ~= "string" or title == "" then - callback(nil, nil) - return - end - - local lookup = title - if season and season > 1 then - lookup = string.format("%s Season %d", lookup, season) - end - local cache_key = string.format("%s|%s", lookup:lower(), tostring(season or "-")) - local cached = mal_lookup_cache[cache_key] - if cached ~= nil then - if cached == false then - callback(nil, lookup) - else - callback(cached, lookup) - end - return - end - - local mal_url = "https://myanimelist.net/search/prefix.json?type=anime&keyword=" .. url_encode(lookup) - run_json_curl_async(mal_url, function(mal_json, mal_error) - if request_id ~= request_generation then - return - end - if not mal_json then - subminer_log("warn", "aniskip", "MAL lookup failed: " .. tostring(mal_error)) - callback(nil, lookup) - return - end - local categories = mal_json.categories - if type(categories) ~= "table" then - mal_lookup_cache[cache_key] = false - callback(nil, lookup) - return - end - - local all_items = {} - for _, category in ipairs(categories) do - if type(category) == "table" and type(category.items) == "table" then - for _, item in ipairs(category.items) do - all_items[#all_items + 1] = item - end - end - end - local best_item = select_best_mal_item(all_items, title, season) - if best_item and tonumber(best_item.id) then - local matched_id = tonumber(best_item.id) - mal_lookup_cache[cache_key] = matched_id - subminer_log( - "info", - "aniskip", - string.format( - 'MAL candidate selected (score-based): id=%s name="%s" season_hint=%s', - tostring(best_item.id), - tostring(best_item.name or ""), - tostring(season or "-") - ) - ) - callback(matched_id, lookup) - return - end - mal_lookup_cache[cache_key] = false - callback(nil, lookup) - end) - end - - local function set_intro_chapters(intro_start, intro_end) - if type(intro_start) ~= "number" or type(intro_end) ~= "number" then - return - end - local current = mp.get_property_native("chapter-list") - local chapters = {} - if type(current) == "table" then - for _, chapter in ipairs(current) do - local title = type(chapter) == "table" and chapter.title or nil - if type(title) ~= "string" or not title:match("^AniSkip ") then - chapters[#chapters + 1] = chapter - end - end - end - chapters[#chapters + 1] = { time = intro_start, title = "AniSkip Intro Start" } - chapters[#chapters + 1] = { time = intro_end, title = "AniSkip Intro End" } - table.sort(chapters, function(a, b) - local a_time = type(a) == "table" and tonumber(a.time) or 0 - local b_time = type(b) == "table" and tonumber(b.time) or 0 - return a_time < b_time - end) - mp.set_property_native("chapter-list", chapters) - end - - local function remove_aniskip_chapters() - local current = mp.get_property_native("chapter-list") - if type(current) ~= "table" then - return - end - local chapters = {} - local changed = false - for _, chapter in ipairs(current) do - local title = type(chapter) == "table" and chapter.title or nil - if type(title) == "string" and title:match("^AniSkip ") then - changed = true - else - chapters[#chapters + 1] = chapter - end - end - if changed then - mp.set_property_native("chapter-list", chapters) - end - end - - local function reset_aniskip_fields() - state.aniskip.prompt_shown = false - state.aniskip.found = false - state.aniskip.mal_id = nil - state.aniskip.title = nil - 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 - - local function clear_aniskip_state() - request_generation = request_generation + 1 - reset_aniskip_fields() - end - - local function skip_intro_now() - if not state.aniskip.found then - show_osd("Intro skip unavailable") - return - end - local intro_start = state.aniskip.intro_start - local intro_end = state.aniskip.intro_end - if type(intro_start) ~= "number" or type(intro_end) ~= "number" then - show_osd("Intro markers missing") - return - end - local now = mp.get_property_number("time-pos") - if type(now) ~= "number" then - show_osd("Skip unavailable") - return - end - local epsilon = 0.35 - if now < (intro_start - epsilon) or now > (intro_end + epsilon) then - show_osd("Skip intro only during intro") - return - end - mp.set_property_number("time-pos", intro_end) - show_osd("Skipped intro") - end - - local function update_intro_button_visibility() - if not opts.aniskip_enabled or not opts.aniskip_show_button or not state.aniskip.found then - return - end - local now = mp.get_property_number("time-pos") - if type(now) ~= "number" then - return - end - local in_intro = now >= (state.aniskip.intro_start or -1) and now < (state.aniskip.intro_end or -1) - local intro_start = state.aniskip.intro_start or -1 - local hint_window_end = intro_start + 3 - if in_intro and not state.aniskip.prompt_shown and now >= intro_start and now < hint_window_end then - local key = opts.aniskip_button_key ~= "" and opts.aniskip_button_key or DEFAULT_ANISKIP_BUTTON_KEY - local message = string.format(opts.aniskip_button_text, key) - mp.osd_message(message, tonumber(opts.aniskip_button_duration) or 3) - state.aniskip.prompt_shown = true - end - end - - local function apply_aniskip_payload(mal_id, title, episode, payload) - local results = payload and payload.results - if type(results) ~= "table" then - return false - end - for _, item in ipairs(results) do - if type(item) == "table" and item.skip_type == "op" and type(item.interval) == "table" then - local intro_start = tonumber(item.interval.start_time) - local intro_end = tonumber(item.interval.end_time) - if intro_start and intro_end and intro_end > intro_start then - state.aniskip.found = true - state.aniskip.mal_id = mal_id - state.aniskip.title = title - state.aniskip.episode = episode - state.aniskip.intro_start = intro_start - 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 %s, ep %s)", - intro_start, - intro_end, - tostring(mal_id or "-"), - tostring(episode or "-") - ) - ) - return true - end - end - end - 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 - return true - end - local forced_mal_id = tonumber(opts.aniskip_mal_id) - if forced_mal_id and forced_mal_id > 0 then - return true - end - local forced_episode = tonumber(opts.aniskip_episode) - if forced_episode and forced_episode > 0 then - return true - end - local forced_season = tonumber(opts.aniskip_season) - if forced_season and forced_season > 0 then - return true - end - if has_launcher_payload() then - return true - end - return false - end - - local function should_fetch_aniskip_async(trigger_source, callback) - if is_remote_media_path() then - callback(false, "remote-url") - return - end - if trigger_source == "script-message" or trigger_source == "overlay-start" then - callback(true, trigger_source) - return - end - if is_launcher_context() then - callback(true, "launcher-context") - return - end - if type(environment.is_subminer_app_running_async) == "function" then - environment.is_subminer_app_running_async(function(running) - if running then - callback(true, "subminer-app-running") - else - callback(false, "subminer-context-missing") - end - end) - return - end - if environment.is_subminer_app_running() then - callback(true, "subminer-app-running") - return - end - callback(false, "subminer-context-missing") - end - - local function resolve_lookup_titles(primary_title) - local media_title_fallback = cleanup_title(mp.get_property("media-title")) - local filename_fallback = cleanup_title(mp.get_property("filename/no-ext") or mp.get_property("filename") or "") - local path_fallback = cleanup_title(mp.get_property("path") or "") - local lookup_titles = {} - local seen_titles = {} - local function push_lookup_title(candidate) - if type(candidate) ~= "string" then - return - end - local trimmed = candidate:match("^%s*(.-)%s*$") or "" - if trimmed == "" then - return - end - local key = trimmed:lower() - if seen_titles[key] then - return - end - seen_titles[key] = true - lookup_titles[#lookup_titles + 1] = trimmed - end - push_lookup_title(primary_title) - push_lookup_title(media_title_fallback) - push_lookup_title(filename_fallback) - push_lookup_title(path_fallback) - return lookup_titles - end - - local function resolve_mal_from_candidates_async(lookup_titles, season, request_id, callback, index, last_lookup) - local current_index = index or 1 - local current_lookup = last_lookup - if current_index > #lookup_titles then - callback(nil, current_lookup) - return - end - local lookup_title = lookup_titles[current_index] - subminer_log("info", "aniskip", string.format('MAL lookup attempt %d/%d using title="%s"', current_index, #lookup_titles, lookup_title)) - resolve_mal_id_async(lookup_title, season, request_id, function(mal_id, lookup) - if request_id ~= request_generation then - return - end - if mal_id then - callback(mal_id, lookup) - return - end - resolve_mal_from_candidates_async(lookup_titles, season, request_id, callback, current_index + 1, lookup or current_lookup) - end) - end - - local function fetch_payload_for_episode_async(mal_id, episode, request_id, callback) - local payload_cache_key = string.format("%d:%d", mal_id, episode) - local cached_payload = payload_cache[payload_cache_key] - if cached_payload ~= nil then - if cached_payload == false then - callback(nil, nil, true) - else - callback(cached_payload, nil, true) - end - return - end - local url = string.format("https://api.aniskip.com/v1/skip-times/%d/%d?types=op&types=ed", mal_id, episode) - subminer_log("info", "aniskip", string.format("AniSkip URL=%s", url)) - run_json_curl_async(url, function(payload, fetch_error) - if request_id ~= request_generation then - return - end - if not payload then - callback(nil, fetch_error, false) - return - end - if payload.found ~= true then - payload_cache[payload_cache_key] = false - callback(nil, nil, false) - return - end - payload_cache[payload_cache_key] = payload - callback(payload, nil, false) - 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 - clear_aniskip_state() - return - end - - should_fetch_aniskip_async(trigger, function(allowed, reason) - if not allowed then - subminer_log("debug", "aniskip", "Skipping lookup: " .. tostring(reason)) - return - end - - request_generation = request_generation + 1 - local request_id = request_generation - 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", - "aniskip", - string.format( - 'Query context: trigger=%s reason=%s title="%s" season=%s episode=%s (opts: title="%s" season=%s episode=%s mal_id=%s; fallback_titles=%d)', - tostring(trigger), - tostring(reason or "-"), - tostring(title or ""), - tostring(season or "-"), - tostring(episode or "-"), - tostring(opts.aniskip_title or ""), - tostring(opts.aniskip_season or "-"), - tostring(opts.aniskip_episode or "-"), - tostring(opts.aniskip_mal_id or "-"), - #lookup_titles - ) - ) - - resolve_mal_from_candidates_async(lookup_titles, season, request_id, function(mal_id, mal_lookup) - if request_id ~= request_generation then - return - end - if not mal_id then - subminer_log("info", "aniskip", string.format('Skipped: MAL id unavailable for query="%s"', tostring(mal_lookup or ""))) - return - end - subminer_log("info", "aniskip", string.format('Resolved MAL id=%d using query="%s"', mal_id, tostring(mal_lookup or ""))) - fetch_payload_for_episode_async(mal_id, episode, request_id, function(payload, fetch_error) - if request_id ~= request_generation then - return - end - if not payload then - if fetch_error then - subminer_log("warn", "aniskip", "AniSkip fetch failed: " .. tostring(fetch_error)) - else - subminer_log("info", "aniskip", "AniSkip: no skip windows found") - 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 - end) - end) - end) - end - - return { - clear_aniskip_state = clear_aniskip_state, - skip_intro_now = skip_intro_now, - update_intro_button_visibility = update_intro_button_visibility, - fetch_aniskip_for_current_media = fetch_aniskip_for_current_media, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/aniskip_match.lua b/.config/mpv/scripts/subminer/aniskip_match.lua deleted file mode 100644 index b33d830..0000000 --- a/.config/mpv/scripts/subminer/aniskip_match.lua +++ /dev/null @@ -1,150 +0,0 @@ -local M = {} - -local function normalize_for_match(value) - if type(value) ~= "string" then - return "" - end - return value:lower():gsub("[^%w]+", " "):gsub("%s+", " "):match("^%s*(.-)%s*$") or "" -end - -local MATCH_STOPWORDS = { - the = true, - this = true, - that = true, - world = true, - animated = true, - series = true, - season = true, - no = true, - on = true, - ["and"] = true, -} - -local function tokenize_match_words(value) - local normalized = normalize_for_match(value) - local tokens = {} - for token in normalized:gmatch("%S+") do - if #token >= 3 and not MATCH_STOPWORDS[token] then - tokens[#tokens + 1] = token - end - end - return tokens -end - -local function token_set(tokens) - local set = {} - for _, token in ipairs(tokens) do - set[token] = true - end - return set -end - -function M.title_overlap_score(expected_title, candidate_title) - local expected = normalize_for_match(expected_title) - local candidate = normalize_for_match(candidate_title) - if expected == "" or candidate == "" then - return 0 - end - if candidate:find(expected, 1, true) then - return 120 - end - local expected_tokens = tokenize_match_words(expected_title) - local candidate_tokens = token_set(tokenize_match_words(candidate_title)) - if #expected_tokens == 0 then - return 0 - end - local score = 0 - local matched = 0 - for _, token in ipairs(expected_tokens) do - if candidate_tokens[token] then - score = score + 30 - matched = matched + 1 - else - score = score - 20 - end - end - if matched == 0 then - score = score - 80 - end - local coverage = matched / #expected_tokens - if #expected_tokens >= 2 then - if coverage >= 0.8 then - score = score + 30 - elseif coverage >= 0.6 then - score = score + 10 - else - score = score - 50 - end - elseif coverage >= 1 then - score = score + 10 - end - return score -end - -local function has_any_sequel_marker(candidate_title) - local normalized = normalize_for_match(candidate_title) - if normalized == "" then - return false - end - local markers = { - "season 2", - "season 3", - "season 4", - "2nd season", - "3rd season", - "4th season", - "second season", - "third season", - "fourth season", - " ii ", - " iii ", - " iv ", - } - local padded = " " .. normalized .. " " - for _, marker in ipairs(markers) do - if padded:find(marker, 1, true) then - return true - end - end - return false -end - -function M.season_signal_score(requested_season, candidate_title) - local season = tonumber(requested_season) - if not season or season < 1 then - return 0 - end - local normalized = " " .. normalize_for_match(candidate_title) .. " " - if normalized == " " then - return 0 - end - - if season == 1 then - return has_any_sequel_marker(candidate_title) and -60 or 20 - end - - local numeric_marker = string.format(" season %d ", season) - local ordinal_marker = string.format(" %dth season ", season) - local roman_markers = { - [2] = { " ii ", " second season ", " 2nd season " }, - [3] = { " iii ", " third season ", " 3rd season " }, - [4] = { " iv ", " fourth season ", " 4th season " }, - [5] = { " v ", " fifth season ", " 5th season " }, - } - - if normalized:find(numeric_marker, 1, true) or normalized:find(ordinal_marker, 1, true) then - return 40 - end - local aliases = roman_markers[season] or {} - for _, marker in ipairs(aliases) do - if normalized:find(marker, 1, true) then - return 40 - end - end - if has_any_sequel_marker(candidate_title) then - return -20 - end - return 5 -end - -return M diff --git a/.config/mpv/scripts/subminer/binary.lua b/.config/mpv/scripts/subminer/binary.lua deleted file mode 100644 index 9b231eb..0000000 --- a/.config/mpv/scripts/subminer/binary.lua +++ /dev/null @@ -1,301 +0,0 @@ -local M = {} - -function M.create(ctx) - local mp = ctx.mp - local utils = ctx.utils - local opts = ctx.opts - local state = ctx.state - local environment = ctx.environment - local subminer_log = ctx.log.subminer_log - - local function normalize_binary_path_candidate(candidate) - if type(candidate) ~= "string" then - return nil - end - local trimmed = candidate:match("^%s*(.-)%s*$") or "" - if trimmed == "" then - return nil - end - if #trimmed >= 2 then - local first = trimmed:sub(1, 1) - local last = trimmed:sub(-1) - if (first == '"' and last == '"') or (first == "'" and last == "'") then - trimmed = trimmed:sub(2, -2) - end - end - return trimmed ~= "" and trimmed or nil - end - - local function binary_candidates_from_app_path(app_path) - if environment.is_windows() then - return { - utils.join_path(app_path, "SubMiner.exe"), - utils.join_path(app_path, "subminer.exe"), - } - end - - return { - utils.join_path(app_path, "Contents", "MacOS", "SubMiner"), - utils.join_path(app_path, "Contents", "MacOS", "subminer"), - } - end - - local function file_exists(path) - local info = utils.file_info(path) - if not info then - return false - end - if info.is_dir ~= nil then - return not info.is_dir - end - return true - end - - local function directory_exists(path) - local info = utils.file_info(path) - return info ~= nil and info.is_dir == true - end - - local function resolve_binary_candidate(candidate) - local normalized = normalize_binary_path_candidate(candidate) - if not normalized then - return nil - end - - if file_exists(normalized) then - return normalized - end - - if environment.is_windows() then - if not normalized:lower():match("%.exe$") then - local with_exe = normalized .. ".exe" - if file_exists(with_exe) then - return with_exe - end - end - - if directory_exists(normalized) then - for _, path in ipairs(binary_candidates_from_app_path(normalized)) do - if file_exists(path) then - return path - end - end - end - - return nil - end - - if not normalized:lower():find("%.app") then - return nil - end - - local app_root = normalized - if not app_root:lower():match("%.app$") then - app_root = normalized:match("(.+%.app)") - end - if not app_root then - return nil - end - - for _, path in ipairs(binary_candidates_from_app_path(app_root)) do - if file_exists(path) then - return path - end - end - - return nil - end - - local function find_binary_override() - for _, env_name in ipairs({ "SUBMINER_APPIMAGE_PATH", "SUBMINER_BINARY_PATH" }) do - local path = resolve_binary_candidate(os.getenv(env_name)) - if path and path ~= "" then - return path - end - end - - return nil - end - - local function add_search_path(search_paths, candidate) - if type(candidate) == "string" and candidate ~= "" then - search_paths[#search_paths + 1] = candidate - end - end - - local function trim_subprocess_stdout(value) - if type(value) ~= "string" then - return nil - end - local trimmed = value:match("^%s*(.-)%s*$") or "" - if trimmed == "" then - return nil - end - return trimmed - end - - local function find_windows_binary_via_system_lookup() - if not environment.is_windows() then - return nil - end - if not mp or type(mp.command_native) ~= "function" then - return nil - end - - local script = [=[ -function Emit-FirstExistingPath { - param([string[]]$Candidates) - - foreach ($candidate in $Candidates) { - if ([string]::IsNullOrWhiteSpace($candidate)) { - continue - } - if (Test-Path -LiteralPath $candidate -PathType Leaf) { - Write-Output $candidate - exit 0 - } - } -} - -$runningProcess = Get-CimInstance Win32_Process | - Where-Object { $_.Name -ieq 'SubMiner.exe' -or $_.Name -ieq 'subminer.exe' } | - Select-Object -First 1 -Property ExecutablePath, CommandLine -if ($null -ne $runningProcess) { - Emit-FirstExistingPath @($runningProcess.ExecutablePath) -} - -$localAppData = [Environment]::GetFolderPath('LocalApplicationData') -$programFiles = [Environment]::GetFolderPath('ProgramFiles') -$programFilesX86 = ${env:ProgramFiles(x86)} - -Emit-FirstExistingPath @( - $(if (-not [string]::IsNullOrWhiteSpace($localAppData)) { Join-Path $localAppData 'Programs\SubMiner\SubMiner.exe' } else { $null }), - $(if (-not [string]::IsNullOrWhiteSpace($programFiles)) { Join-Path $programFiles 'SubMiner\SubMiner.exe' } else { $null }), - $(if (-not [string]::IsNullOrWhiteSpace($programFilesX86)) { Join-Path $programFilesX86 'SubMiner\SubMiner.exe' } else { $null }), - 'C:\SubMiner\SubMiner.exe' -) - -foreach ($registryPath in @( - 'HKCU:\Software\Microsoft\Windows\CurrentVersion\App Paths\SubMiner.exe', - 'HKLM:\Software\Microsoft\Windows\CurrentVersion\App Paths\SubMiner.exe', - 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\SubMiner.exe' -)) { - try { - $appPath = (Get-ItemProperty -Path $registryPath -ErrorAction Stop).'(default)' - Emit-FirstExistingPath @($appPath) - } catch { - } -} - -try { - $commandPath = Get-Command SubMiner.exe -ErrorAction Stop | Select-Object -First 1 -ExpandProperty Source - Emit-FirstExistingPath @($commandPath) -} catch { -} -]=] - - local result = mp.command_native({ - name = "subprocess", - args = { - "powershell.exe", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - script, - }, - playback_only = false, - capture_stdout = true, - capture_stderr = false, - }) - if not result or result.status ~= 0 then - return nil - end - - local candidate = trim_subprocess_stdout(result.stdout) - if not candidate then - return nil - end - - return resolve_binary_candidate(candidate) - end - - local function find_binary() - local override = find_binary_override() - if override then - return override - end - - local configured = resolve_binary_candidate(opts.binary_path) - if configured then - return configured - end - - local system_lookup_binary = find_windows_binary_via_system_lookup() - if system_lookup_binary then - subminer_log("info", "binary", "Found Windows binary via system lookup at: " .. system_lookup_binary) - return system_lookup_binary - end - - local home = os.getenv("HOME") or os.getenv("USERPROFILE") or "" - local app_data = os.getenv("APPDATA") or "" - local app_data_local = app_data ~= "" and app_data:gsub("[/\\][Rr][Oo][Aa][Mm][Ii][Nn][Gg]$", "\\Local") or "" - local local_app_data = os.getenv("LOCALAPPDATA") or utils.join_path(home, "AppData", "Local") - local program_files = os.getenv("ProgramFiles") or "C:\\Program Files" - local program_files_x86 = os.getenv("ProgramFiles(x86)") or "C:\\Program Files (x86)" - local search_paths = {} - - if environment.is_windows() then - add_search_path(search_paths, utils.join_path(app_data_local, "Programs", "SubMiner", "SubMiner.exe")) - add_search_path(search_paths, utils.join_path(local_app_data, "Programs", "SubMiner", "SubMiner.exe")) - add_search_path(search_paths, utils.join_path(program_files, "SubMiner", "SubMiner.exe")) - add_search_path(search_paths, utils.join_path(program_files_x86, "SubMiner", "SubMiner.exe")) - add_search_path(search_paths, "C:\\SubMiner\\SubMiner.exe") - else - add_search_path(search_paths, "/Applications/SubMiner.app/Contents/MacOS/SubMiner") - add_search_path(search_paths, utils.join_path(home, "Applications", "SubMiner.app", "Contents", "MacOS", "SubMiner")) - add_search_path(search_paths, utils.join_path(home, ".local", "bin", "SubMiner.AppImage")) - add_search_path(search_paths, "/opt/SubMiner/SubMiner.AppImage") - add_search_path(search_paths, "/usr/local/bin/SubMiner") - add_search_path(search_paths, "/usr/local/bin/subminer") - add_search_path(search_paths, "/usr/bin/SubMiner") - add_search_path(search_paths, "/usr/bin/subminer") - end - - for _, path in ipairs(search_paths) do - if file_exists(path) then - subminer_log("info", "binary", "Found binary at: " .. path) - return path - end - end - - return nil - end - - local function ensure_binary_available() - if state.binary_available and state.binary_path and file_exists(state.binary_path) then - return true - end - - local discovered = find_binary() - if discovered then - state.binary_path = discovered - state.binary_available = true - return true - end - - state.binary_path = nil - state.binary_available = false - return false - end - - return { - normalize_binary_path_candidate = normalize_binary_path_candidate, - file_exists = file_exists, - find_binary = find_binary, - ensure_binary_available = ensure_binary_available, - is_windows = environment.is_windows, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/bootstrap.lua b/.config/mpv/scripts/subminer/bootstrap.lua deleted file mode 100644 index 62eaabf..0000000 --- a/.config/mpv/scripts/subminer/bootstrap.lua +++ /dev/null @@ -1,80 +0,0 @@ -local M = {} -local BOOTSTRAP_GUARD_KEY = "__subminer_plugin_bootstrapped" - -function M.init() - if rawget(_G, BOOTSTRAP_GUARD_KEY) == true then - return - end - rawset(_G, BOOTSTRAP_GUARD_KEY, true) - - local input = require("mp.input") - local mp = require("mp") - local msg = require("mp.msg") - local options_lib = require("mp.options") - local utils = require("mp.utils") - - local options_helper = require("options") - local environment = require("environment").create({ mp = mp }) - local opts = options_helper.load(options_lib, environment.default_socket_path()) - local state = require("state").new() - - local ctx = { - input = input, - mp = mp, - msg = msg, - utils = utils, - opts = opts, - state = state, - options_helper = options_helper, - environment = environment, - } - - local instances = {} - - local function lazy_instance(key, factory) - if instances[key] == nil then - instances[key] = factory() - end - return instances[key] - end - - local function make_lazy_proxy(key, factory) - return setmetatable({}, { - __index = function(_, member) - return lazy_instance(key, factory)[member] - end, - }) - end - - ctx.log = make_lazy_proxy("log", function() - return require("log").create(ctx) - end) - ctx.binary = make_lazy_proxy("binary", function() - return require("binary").create(ctx) - end) - ctx.aniskip = make_lazy_proxy("aniskip", function() - return require("aniskip").create(ctx) - end) - ctx.hover = make_lazy_proxy("hover", function() - return require("hover").create(ctx) - end) - ctx.process = make_lazy_proxy("process", function() - return require("process").create(ctx) - end) - ctx.ui = make_lazy_proxy("ui", function() - return require("ui").create(ctx) - end) - ctx.messages = make_lazy_proxy("messages", function() - return require("messages").create(ctx) - end) - ctx.lifecycle = make_lazy_proxy("lifecycle", function() - return require("lifecycle").create(ctx) - end) - - ctx.ui.register_keybindings() - ctx.messages.register_script_messages() - ctx.lifecycle.register_lifecycle_hooks() - ctx.log.subminer_log("info", "lifecycle", "SubMiner plugin loaded") -end - -return M diff --git a/.config/mpv/scripts/subminer/environment.lua b/.config/mpv/scripts/subminer/environment.lua deleted file mode 100644 index 3aa6f79..0000000 --- a/.config/mpv/scripts/subminer/environment.lua +++ /dev/null @@ -1,210 +0,0 @@ -local M = {} - -function M.create(ctx) - local mp = ctx.mp - - local detected_backend = nil - local app_running_cache_value = nil - local app_running_cache_time = nil - local app_running_check_inflight = false - local app_running_waiters = {} - local APP_RUNNING_CACHE_TTL_SECONDS = 2 - - local function is_windows() - return package.config:sub(1, 1) == "\\" - end - - local function is_macos() - local platform = mp.get_property("platform") or "" - if platform == "macos" or platform == "darwin" then - return true - end - local ostype = os.getenv("OSTYPE") or "" - return ostype:find("darwin") ~= nil - end - - local function default_socket_path() - if is_windows() then - return "\\\\.\\pipe\\subminer-socket" - end - return "/tmp/subminer-socket" - end - - local function is_linux() - return not is_windows() and not is_macos() - end - - local function now_seconds() - if type(mp.get_time) == "function" then - local value = tonumber(mp.get_time()) - if value then - return value - end - end - return os.time() - end - - local function process_list_has_subminer(raw_process_list) - if type(raw_process_list) ~= "string" then - return false - end - local process_list = raw_process_list:lower() - for line in process_list:gmatch("[^\n]+") do - if is_windows() then - local image = line:match('^"([^"]+)","') - if not image then - image = line:match('^"([^"]+)"') - end - if not image then - goto continue - end - if image == "subminer" or image == "subminer.exe" or image == "subminer.appimage" or image == "subminer.app" then - return true - end - if image:find("subminer", 1, true) and not image:find(".lua", 1, true) then - return true - end - else - local argv0 = line:match('^"([^"]+)"') or line:match("^%s*([^%s]+)") - if not argv0 then - goto continue - end - if argv0:find("subminer.lua", 1, true) or argv0:find("subminer.conf", 1, true) then - goto continue - end - local exe = argv0:match("([^/\\]+)$") or argv0 - if exe == "SubMiner" or exe == "SubMiner.AppImage" or exe == "SubMiner.exe" or exe == "subminer" or exe == "subminer.appimage" or exe == "subminer.exe" then - return true - end - if exe:find("subminer", 1, true) and exe:find("%.lua", 1, true) == nil and exe:find("%.app", 1, true) == nil then - return true - end - end - - ::continue:: - end - return false - end - - local function process_scan_command() - if is_windows() then - return { "tasklist", "/FO", "CSV", "/NH" } - end - return { "ps", "-A", "-o", "args=" } - end - - local function is_subminer_process_running() - local result = mp.command_native({ - name = "subprocess", - args = process_scan_command(), - playback_only = false, - capture_stdout = true, - capture_stderr = false, - }) - if not result or result.status ~= 0 then - return false - end - return process_list_has_subminer(result.stdout) - end - - local function flush_app_running_waiters(value) - local waiters = app_running_waiters - app_running_waiters = {} - for _, waiter in ipairs(waiters) do - waiter(value) - end - end - - local function is_subminer_app_running_async(callback, opts) - opts = opts or {} - local force_refresh = opts.force_refresh == true - local now = now_seconds() - if not force_refresh and app_running_cache_value ~= nil and app_running_cache_time ~= nil then - if (now - app_running_cache_time) <= APP_RUNNING_CACHE_TTL_SECONDS then - callback(app_running_cache_value) - return - end - end - - app_running_waiters[#app_running_waiters + 1] = callback - if app_running_check_inflight then - return - end - app_running_check_inflight = true - - mp.command_native_async({ - name = "subprocess", - args = process_scan_command(), - playback_only = false, - capture_stdout = true, - capture_stderr = false, - }, function(success, result) - app_running_check_inflight = false - local running = false - if success and result and result.status == 0 then - running = process_list_has_subminer(result.stdout) - end - app_running_cache_value = running - app_running_cache_time = now_seconds() - flush_app_running_waiters(running) - end) - end - - local function is_subminer_app_running() - local running = is_subminer_process_running() - app_running_cache_value = running - app_running_cache_time = now_seconds() - return running - end - - local function set_subminer_app_running_cache(running) - app_running_cache_value = running == true - app_running_cache_time = now_seconds() - end - - local function detect_backend() - if detected_backend then - return detected_backend - end - - local backend = nil - local subminer_log = ctx.log and ctx.log.subminer_log or function() end - - if is_macos() then - backend = "macos" - elseif is_windows() then - backend = nil - elseif os.getenv("HYPRLAND_INSTANCE_SIGNATURE") then - backend = "hyprland" - elseif os.getenv("SWAYSOCK") then - backend = "sway" - elseif os.getenv("XDG_SESSION_TYPE") == "x11" or os.getenv("DISPLAY") then - backend = "x11" - else - subminer_log("warn", "backend", "Could not detect window manager, falling back to x11") - backend = "x11" - end - - detected_backend = backend - if backend then - subminer_log("info", "backend", "Detected backend: " .. backend) - else - subminer_log("info", "backend", "No backend detected") - end - return backend - end - - return { - is_windows = is_windows, - is_macos = is_macos, - is_linux = is_linux, - default_socket_path = default_socket_path, - is_subminer_process_running = is_subminer_process_running, - is_subminer_app_running = is_subminer_app_running, - is_subminer_app_running_async = is_subminer_app_running_async, - set_subminer_app_running_cache = set_subminer_app_running_cache, - detect_backend = detect_backend, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/hover.lua b/.config/mpv/scripts/subminer/hover.lua deleted file mode 100644 index 6a24e41..0000000 --- a/.config/mpv/scripts/subminer/hover.lua +++ /dev/null @@ -1,431 +0,0 @@ -local M = {} - -local DEFAULT_HOVER_BASE_COLOR = "FFFFFF" -local DEFAULT_HOVER_COLOR = "C6A0F6" - -function M.create(ctx) - local mp = ctx.mp - local msg = ctx.msg - local utils = ctx.utils - local state = ctx.state - - local function to_hex_color(input) - if type(input) ~= "string" then - return nil - end - - local hex = input:gsub("[%#%']", ""):gsub("^0x", "") - if #hex ~= 6 and #hex ~= 3 then - return nil - end - if #hex == 3 then - return hex:sub(1, 1) .. hex:sub(1, 1) .. hex:sub(2, 2) .. hex:sub(2, 2) .. hex:sub(3, 3) .. hex:sub(3, 3) - end - return hex - end - - local function fix_ass_color(input, fallback) - local hex = to_hex_color(input) - if not hex then - return fallback or DEFAULT_HOVER_BASE_COLOR - end - local r, g, b = hex:sub(1, 2), hex:sub(3, 4), hex:sub(5, 6) - return b .. g .. r - end - - local function sanitize_hover_ass_color(input, fallback_rgb) - local fallback = fix_ass_color(fallback_rgb or DEFAULT_HOVER_COLOR, DEFAULT_HOVER_COLOR) - local converted = fix_ass_color(input, fallback) - if converted == "000000" then - return fallback - end - return converted - end - - local function escape_ass_text(text) - return (text or ""):gsub("\\", "\\\\"):gsub("{", "\\{"):gsub("}", "\\}"):gsub("\n", "\\N") - end - - local function resolve_osd_dimensions() - local width = mp.get_property_number("osd-width", 0) or 0 - local height = mp.get_property_number("osd-height", 0) or 0 - - if width <= 0 or height <= 0 then - local osd_dims = mp.get_property_native("osd-dimensions") - if type(osd_dims) == "table" and type(osd_dims.w) == "number" and osd_dims.w > 0 then - width = osd_dims.w - end - if type(osd_dims) == "table" and type(osd_dims.h) == "number" and osd_dims.h > 0 then - height = osd_dims.h - end - end - - if width <= 0 then - width = 1280 - end - if height <= 0 then - height = 720 - end - - return width, height - end - - local function resolve_metrics() - local sub_font_size = mp.get_property_number("sub-font-size", 36) or 36 - local sub_scale = mp.get_property_number("sub-scale", 1) or 1 - local sub_scale_by_window = mp.get_property_bool("sub-scale-by-window", true) == true - local sub_pos = mp.get_property_number("sub-pos", 100) or 100 - local sub_margin_y = mp.get_property_number("sub-margin-y", 0) or 0 - local sub_font = mp.get_property("sub-font", "sans-serif") or "sans-serif" - local sub_spacing = mp.get_property_number("sub-spacing", 0) or 0 - local sub_bold = mp.get_property_bool("sub-bold", false) == true - local sub_italic = mp.get_property_bool("sub-italic", false) == true - local sub_border_size = mp.get_property_number("sub-border-size", 2) or 2 - local sub_shadow_offset = mp.get_property_number("sub-shadow-offset", 0) or 0 - local osd_w, osd_h = resolve_osd_dimensions() - local window_scale = 1 - if sub_scale_by_window and osd_h > 0 then - window_scale = osd_h / 720 - end - local effective_margin_y = sub_margin_y * window_scale - - return { - font_size = sub_font_size * (sub_scale > 0 and sub_scale or 1) * window_scale, - pos = sub_pos, - margin_y = effective_margin_y, - font = sub_font, - spacing = sub_spacing, - bold = sub_bold, - italic = sub_italic, - border = sub_border_size * window_scale, - shadow = sub_shadow_offset * window_scale, - base_color = fix_ass_color(mp.get_property("sub-color"), DEFAULT_HOVER_BASE_COLOR), - hover_color = sanitize_hover_ass_color(nil, DEFAULT_HOVER_COLOR), - } - end - - local function get_subtitle_ass_property() - local ass_text = mp.get_property("sub-text/ass") - if type(ass_text) == "string" and ass_text ~= "" then - return ass_text - end - ass_text = mp.get_property("sub-text-ass") - if type(ass_text) == "string" and ass_text ~= "" then - return ass_text - end - return nil - end - - local function plain_text_and_ass_map(text) - local plain = {} - local map = {} - local plain_len = 0 - local i = 1 - local text_len = #text - - while i <= text_len do - local ch = text:sub(i, i) - if ch == "{" then - local close = text:find("}", i + 1, true) - if not close then - break - end - i = close + 1 - elseif ch == "\\" then - local esc = text:sub(i + 1, i + 1) - if esc == "N" or esc == "n" then - plain_len = plain_len + 1 - plain[plain_len] = "\n" - map[plain_len] = i - i = i + 2 - elseif esc == "h" then - plain_len = plain_len + 1 - plain[plain_len] = " " - map[plain_len] = i - i = i + 2 - elseif esc == "{" then - plain_len = plain_len + 1 - plain[plain_len] = "{" - map[plain_len] = i - i = i + 2 - elseif esc == "}" then - plain_len = plain_len + 1 - plain[plain_len] = "}" - map[plain_len] = i - i = i + 2 - elseif esc == "\\" then - plain_len = plain_len + 1 - plain[plain_len] = "\\" - map[plain_len] = i - i = i + 2 - else - local seq_end = i + 1 - while seq_end <= text_len and text:sub(seq_end, seq_end):match("[%a]") do - seq_end = seq_end + 1 - end - if text:sub(seq_end, seq_end) == "(" then - local close = text:find(")", seq_end, true) - if close then - i = close + 1 - else - i = seq_end + 1 - end - else - i = seq_end + 1 - end - end - else - plain_len = plain_len + 1 - plain[plain_len] = ch - map[plain_len] = i - i = i + 1 - end - end - - return table.concat(plain), map - end - - local function find_hover_span(payload, plain) - local source_len = #plain - local cursor = 1 - for _, token in ipairs(payload.tokens or {}) do - if type(token) ~= "table" or type(token.text) ~= "string" or token.text == "" then - goto continue - end - - local token_text = token.text - local start_pos = nil - local end_pos = nil - - if type(token.startPos) == "number" and type(token.endPos) == "number" then - if token.startPos >= 0 and token.endPos >= token.startPos then - local candidate_start = token.startPos + 1 - local candidate_stop = token.endPos - if candidate_start >= 1 and candidate_stop <= source_len and candidate_stop >= candidate_start and plain:sub(candidate_start, candidate_stop) == token_text then - start_pos = candidate_start - end_pos = candidate_stop - end - end - end - - if not start_pos or not end_pos then - local fallback_start, fallback_stop = plain:find(token_text, cursor, true) - if not fallback_start then - fallback_start, fallback_stop = plain:find(token_text, 1, true) - end - start_pos, end_pos = fallback_start, fallback_stop - end - - if start_pos and end_pos then - if token.index == payload.hoveredTokenIndex then - return start_pos, end_pos - end - cursor = end_pos + 1 - end - - ::continue:: - end - - return nil - end - - local function inject_hover_color_to_ass(raw_ass, plain_map, hover_start, hover_end, hover_color, base_color) - if hover_start == nil or hover_end == nil then - return raw_ass - end - - local raw_open_idx = plain_map[hover_start] or 1 - local raw_close_idx = plain_map[hover_end + 1] or (#raw_ass + 1) - if raw_open_idx < 1 then - raw_open_idx = 1 - end - if raw_close_idx < 1 then - raw_close_idx = 1 - end - if raw_open_idx > #raw_ass + 1 then - raw_open_idx = #raw_ass + 1 - end - if raw_close_idx > #raw_ass + 1 then - raw_close_idx = #raw_ass + 1 - end - - local before = raw_ass:sub(1, raw_open_idx - 1) - local hovered = raw_ass:sub(raw_open_idx, raw_close_idx - 1) - local after = raw_ass:sub(raw_close_idx) - local hover_suffix = string.format("\\1c&H%s&", hover_color) - - -- Keep hover foreground stable even when inline ASS override tags (\1c/\c/\r) appear inside token. - hovered = hovered:gsub("{([^}]*)}", function(inner) - if inner:find("\\1c&H", 1, true) or inner:find("\\c&H", 1, true) or inner:find("\\r", 1, true) then - return "{" .. inner .. hover_suffix .. "}" - end - return "{" .. inner .. "}" - end) - - local open_tag = string.format("{\\1c&H%s&}", hover_color) - local close_tag = string.format("{\\1c&H%s&}", base_color) - return before .. open_tag .. hovered .. close_tag .. after - end - - local function build_hover_subtitle_content(payload) - local source_ass = get_subtitle_ass_property() - if type(source_ass) == "string" and source_ass ~= "" then - state.hover_highlight.cached_ass = source_ass - else - source_ass = state.hover_highlight.cached_ass - end - if type(source_ass) ~= "string" or source_ass == "" then - return nil - end - - local plain_source, plain_map = plain_text_and_ass_map(source_ass) - if type(plain_source) ~= "string" or plain_source == "" then - return nil - end - - local hover_start, hover_end = find_hover_span(payload, plain_source) - if not hover_start or not hover_end then - return nil - end - - local metrics = resolve_metrics() - local hover_color = sanitize_hover_ass_color(payload.colors and payload.colors.hover or nil, DEFAULT_HOVER_COLOR) - local base_color = fix_ass_color(payload.colors and payload.colors.base or nil, metrics.base_color) - return inject_hover_color_to_ass(source_ass, plain_map, hover_start, hover_end, hover_color, base_color) - end - - local function clear_hover_overlay() - if state.hover_highlight.clear_timer then - state.hover_highlight.clear_timer:kill() - state.hover_highlight.clear_timer = nil - end - if state.hover_highlight.overlay_active then - if type(state.hover_highlight.saved_sub_visibility) == "string" then - mp.set_property("sub-visibility", state.hover_highlight.saved_sub_visibility) - else - mp.set_property("sub-visibility", "yes") - end - if type(state.hover_highlight.saved_secondary_sub_visibility) == "string" then - mp.set_property("secondary-sub-visibility", state.hover_highlight.saved_secondary_sub_visibility) - end - state.hover_highlight.saved_sub_visibility = nil - state.hover_highlight.saved_secondary_sub_visibility = nil - state.hover_highlight.overlay_active = false - end - mp.set_osd_ass(0, 0, "") - state.hover_highlight.payload = nil - state.hover_highlight.revision = -1 - state.hover_highlight.cached_ass = nil - state.hover_highlight.last_hover_update_ts = 0 - end - - local function schedule_hover_clear(delay_seconds) - if state.hover_highlight.clear_timer then - state.hover_highlight.clear_timer:kill() - state.hover_highlight.clear_timer = nil - end - state.hover_highlight.clear_timer = mp.add_timeout(delay_seconds or 0.08, function() - state.hover_highlight.clear_timer = nil - clear_hover_overlay() - end) - end - - local function render_hover_overlay(payload) - if not payload or payload.hoveredTokenIndex == nil or payload.subtitle == nil then - clear_hover_overlay() - return - end - - local ass = build_hover_subtitle_content(payload) - if not ass then - return - end - - local osd_w, osd_h = resolve_osd_dimensions() - local metrics = resolve_metrics() - local osd_dims = mp.get_property_native("osd-dimensions") - local ml = (type(osd_dims) == "table" and type(osd_dims.ml) == "number") and osd_dims.ml or 0 - local mr = (type(osd_dims) == "table" and type(osd_dims.mr) == "number") and osd_dims.mr or 0 - local mt = (type(osd_dims) == "table" and type(osd_dims.mt) == "number") and osd_dims.mt or 0 - local mb = (type(osd_dims) == "table" and type(osd_dims.mb) == "number") and osd_dims.mb or 0 - local usable_w = math.max(1, osd_w - ml - mr) - local usable_h = math.max(1, osd_h - mt - mb) - local anchor_x = math.floor(ml + usable_w / 2) - local baseline_adjust = (metrics.border + metrics.shadow) * 5 - local anchor_y = math.floor(mt + (usable_h * metrics.pos / 100) - metrics.margin_y + baseline_adjust) - local font_size = math.max(8, metrics.font_size) - local anchor_tag = string.format( - "{\\an2\\q2\\pos(%d,%d)\\fn%s\\fs%g\\b%d\\i%d\\fsp%g\\bord%g\\shad%g\\1c&H%s&}", - anchor_x, - anchor_y, - escape_ass_text(metrics.font), - font_size, - metrics.bold and 1 or 0, - metrics.italic and 1 or 0, - metrics.spacing, - metrics.border, - metrics.shadow, - metrics.base_color - ) - if not state.hover_highlight.overlay_active then - state.hover_highlight.saved_sub_visibility = mp.get_property("sub-visibility") - state.hover_highlight.saved_secondary_sub_visibility = mp.get_property("secondary-sub-visibility") - mp.set_property("sub-visibility", "no") - mp.set_property("secondary-sub-visibility", "no") - state.hover_highlight.overlay_active = true - end - mp.set_osd_ass(osd_w, osd_h, anchor_tag .. ass) - end - - local function handle_hover_message(payload_json) - local parsed, parse_error = utils.parse_json(payload_json) - if not parsed then - msg.warn("Invalid hover-highlight payload: " .. tostring(parse_error)) - clear_hover_overlay() - return - end - - if type(parsed.revision) ~= "number" then - clear_hover_overlay() - return - end - - if parsed.revision < state.hover_highlight.revision then - return - end - - if type(parsed.hoveredTokenIndex) == "number" and type(parsed.tokens) == "table" then - if state.hover_highlight.clear_timer then - state.hover_highlight.clear_timer:kill() - state.hover_highlight.clear_timer = nil - end - state.hover_highlight.revision = parsed.revision - state.hover_highlight.payload = parsed - state.hover_highlight.last_hover_update_ts = mp.get_time() or 0 - render_hover_overlay(state.hover_highlight.payload) - return - end - - local now = mp.get_time() or 0 - local elapsed_since_hover = now - (state.hover_highlight.last_hover_update_ts or 0) - state.hover_highlight.revision = parsed.revision - state.hover_highlight.payload = nil - if state.hover_highlight.overlay_active then - if elapsed_since_hover > 0.35 then - return - end - schedule_hover_clear(0.08) - else - clear_hover_overlay() - end - end - - return { - HOVER_MESSAGE_NAME = "subminer-hover-token", - HOVER_MESSAGE_NAME_LEGACY = "yomipv-hover-token", - handle_hover_message = handle_hover_message, - clear_hover_overlay = clear_hover_overlay, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/init.lua b/.config/mpv/scripts/subminer/init.lua deleted file mode 100644 index 0371a87..0000000 --- a/.config/mpv/scripts/subminer/init.lua +++ /dev/null @@ -1,7 +0,0 @@ -local M = {} - -function M.init() - require("bootstrap").init() -end - -return M diff --git a/.config/mpv/scripts/subminer/lifecycle.lua b/.config/mpv/scripts/subminer/lifecycle.lua deleted file mode 100644 index c94e2d5..0000000 --- a/.config/mpv/scripts/subminer/lifecycle.lua +++ /dev/null @@ -1,111 +0,0 @@ -local M = {} - -function M.create(ctx) - local mp = ctx.mp - local opts = ctx.opts - local state = ctx.state - local options_helper = ctx.options_helper - local process = ctx.process - local aniskip = ctx.aniskip - local hover = ctx.hover - local subminer_log = ctx.log.subminer_log - local show_osd = ctx.log.show_osd - - local function schedule_aniskip_fetch(trigger_source, delay_seconds) - local delay = tonumber(delay_seconds) or 0 - mp.add_timeout(delay, function() - aniskip.fetch_aniskip_for_current_media(trigger_source) - end) - end - - local function resolve_auto_start_enabled() - local raw_auto_start = opts.auto_start - if raw_auto_start == nil then - raw_auto_start = opts.auto_start_overlay - end - if raw_auto_start == nil then - raw_auto_start = opts["auto-start"] - end - return options_helper.coerce_bool(raw_auto_start, false) - end - - local function on_file_loaded() - aniskip.clear_aniskip_state() - process.disarm_auto_play_ready_gate() - - local should_auto_start = resolve_auto_start_enabled() - if should_auto_start then - if not process.has_matching_mpv_ipc_socket(opts.socket_path) then - subminer_log( - "info", - "lifecycle", - "Skipping auto-start: input-ipc-server does not match configured socket_path" - ) - schedule_aniskip_fetch("file-loaded", 0) - return - end - - process.start_overlay({ - auto_start_trigger = true, - socket_path = opts.socket_path, - }) - -- Give the overlay process a moment to initialize before querying AniSkip. - schedule_aniskip_fetch("overlay-start", 0.8) - return - end - - schedule_aniskip_fetch("file-loaded", 0) - end - - local function on_shutdown() - aniskip.clear_aniskip_state() - hover.clear_hover_overlay() - process.disarm_auto_play_ready_gate() - if state.overlay_running then - subminer_log("info", "lifecycle", "mpv shutting down, hiding SubMiner overlay") - process.hide_visible_overlay() - end - end - - local function register_lifecycle_hooks() - mp.register_event("file-loaded", on_file_loaded) - mp.register_event("shutdown", on_shutdown) - mp.register_event("file-loaded", function() - hover.clear_hover_overlay() - end) - mp.register_event("end-file", function() - process.disarm_auto_play_ready_gate() - hover.clear_hover_overlay() - if state.overlay_running then - process.hide_visible_overlay() - end - end) - mp.register_event("shutdown", function() - hover.clear_hover_overlay() - end) - mp.register_event("end-file", function() - aniskip.clear_aniskip_state() - end) - mp.register_event("shutdown", function() - aniskip.clear_aniskip_state() - end) - mp.add_hook("on_unload", 10, function() - hover.clear_hover_overlay() - aniskip.clear_aniskip_state() - end) - mp.observe_property("sub-start", "native", function() - hover.clear_hover_overlay() - end) - mp.observe_property("time-pos", "number", function() - aniskip.update_intro_button_visibility() - end) - end - - return { - on_file_loaded = on_file_loaded, - on_shutdown = on_shutdown, - register_lifecycle_hooks = register_lifecycle_hooks, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/log.lua b/.config/mpv/scripts/subminer/log.lua deleted file mode 100644 index 57edde8..0000000 --- a/.config/mpv/scripts/subminer/log.lua +++ /dev/null @@ -1,67 +0,0 @@ -local M = {} - -local LOG_LEVEL_PRIORITY = { - debug = 10, - info = 20, - warn = 30, - error = 40, -} - -function M.create(ctx) - local mp = ctx.mp - local msg = ctx.msg - local opts = ctx.opts - - local function normalize_log_level(level) - local normalized = (level or "info"):lower() - if LOG_LEVEL_PRIORITY[normalized] then - return normalized - end - return "info" - end - - local function should_log(level) - local current = normalize_log_level(opts.log_level) - local target = normalize_log_level(level) - return LOG_LEVEL_PRIORITY[target] >= LOG_LEVEL_PRIORITY[current] - end - - local function subminer_log(level, scope, message) - if not should_log(level) then - return - end - local timestamp = os.date("%Y-%m-%d %H:%M:%S") - local line = string.format("[subminer] - %s - %s - [%s] %s", timestamp, string.upper(level), scope, message) - if level == "error" then - msg.error(line) - elseif level == "warn" then - msg.warn(line) - elseif level == "debug" then - msg.debug(line) - else - msg.info(line) - end - end - - local function show_osd(message) - if opts.osd_messages then - local payload = "SubMiner: " .. message - local sent = false - if type(mp.osd_message) == "function" then - sent = pcall(mp.osd_message, payload, 3) - end - if not sent and type(mp.commandv) == "function" then - pcall(mp.commandv, "show-text", payload, "3000") - end - end - end - - return { - normalize_log_level = normalize_log_level, - should_log = should_log, - subminer_log = subminer_log, - show_osd = show_osd, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/main.lua b/.config/mpv/scripts/subminer/main.lua deleted file mode 100644 index 62ed65f..0000000 --- a/.config/mpv/scripts/subminer/main.lua +++ /dev/null @@ -1,30 +0,0 @@ -local mp = require("mp") - -local function current_script_dir() - if type(mp.get_script_directory) == "function" then - local from_mpv = mp.get_script_directory() - if type(from_mpv) == "string" and from_mpv ~= "" then - return from_mpv - end - end - - local source = debug.getinfo(1, "S").source or "" - if source:sub(1, 1) == "@" then - local full = source:sub(2) - return full:match("^(.*)[/\\][^/\\]+$") or "." - end - return "." -end - -local script_dir = current_script_dir() -local module_patterns = script_dir .. "/?.lua;" .. script_dir .. "/?/init.lua;" -if not package.path:find(module_patterns, 1, true) then - package.path = module_patterns .. package.path -end - -local init_module = assert(loadfile(script_dir .. "/init.lua"))() -if type(init_module) == "table" and type(init_module.init) == "function" then - init_module.init() -elseif type(init_module) == "function" then - init_module() -end diff --git a/.config/mpv/scripts/subminer/messages.lua b/.config/mpv/scripts/subminer/messages.lua deleted file mode 100644 index 44c5ade..0000000 --- a/.config/mpv/scripts/subminer/messages.lua +++ /dev/null @@ -1,57 +0,0 @@ -local M = {} - -function M.create(ctx) - local mp = ctx.mp - local process = ctx.process - local aniskip = ctx.aniskip - local hover = ctx.hover - local ui = ctx.ui - - local function register_script_messages() - mp.register_script_message("subminer-start", function(...) - process.start_overlay_from_script_message(...) - end) - mp.register_script_message("subminer-stop", function() - process.stop_overlay() - end) - mp.register_script_message("subminer-toggle", function() - process.toggle_overlay() - end) - mp.register_script_message("subminer-menu", function() - ui.show_menu() - end) - mp.register_script_message("subminer-options", function() - process.open_options() - end) - mp.register_script_message("subminer-restart", function() - process.restart_overlay() - end) - mp.register_script_message("subminer-status", function() - process.check_status() - end) - mp.register_script_message("subminer-autoplay-ready", function() - process.notify_auto_play_ready() - end) - mp.register_script_message("subminer-aniskip-refresh", function() - aniskip.fetch_aniskip_for_current_media("script-message") - end) - mp.register_script_message("subminer-skip-intro", function() - aniskip.skip_intro_now() - end) - mp.register_script_message(hover.HOVER_MESSAGE_NAME, function(payload_json) - hover.handle_hover_message(payload_json) - end) - mp.register_script_message(hover.HOVER_MESSAGE_NAME_LEGACY, function(payload_json) - hover.handle_hover_message(payload_json) - end) - mp.register_script_message("subminer-stats-toggle", function() - mp.osd_message("Stats: press ` (backtick) in overlay", 3) - end) - end - - return { - register_script_messages = register_script_messages, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/options.lua b/.config/mpv/scripts/subminer/options.lua deleted file mode 100644 index f084314..0000000 --- a/.config/mpv/scripts/subminer/options.lua +++ /dev/null @@ -1,72 +0,0 @@ -local M = {} -local DEFAULT_ANISKIP_BUTTON_KEY = "TAB" - -local function normalize_socket_path_option(socket_path, default_socket_path) - if type(default_socket_path) ~= "string" then - return socket_path - end - - local trimmed_default = default_socket_path:match("^%s*(.-)%s*$") - local trimmed_socket = type(socket_path) == "string" and socket_path:match("^%s*(.-)%s*$") or socket_path - if trimmed_default ~= "\\\\.\\pipe\\subminer-socket" then - return trimmed_socket - end - if type(trimmed_socket) ~= "string" or trimmed_socket == "" then - return trimmed_default - end - if trimmed_socket == "/tmp/subminer-socket" or trimmed_socket == "\\tmp\\subminer-socket" then - return trimmed_default - end - if trimmed_socket == "\\\\.\\pipe\\tmp\\subminer-socket" then - return trimmed_default - end - return trimmed_socket -end - -function M.load(options_lib, default_socket_path) - local opts = { - binary_path = "", - socket_path = default_socket_path, - texthooker_enabled = true, - texthooker_port = 5174, - backend = "auto", - auto_start = true, - auto_start_visible_overlay = true, - auto_start_pause_until_ready = true, - auto_start_pause_until_ready_timeout_seconds = 15, - osd_messages = true, - log_level = "info", - aniskip_enabled = true, - aniskip_title = "", - 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 = DEFAULT_ANISKIP_BUTTON_KEY, - aniskip_button_duration = 3, - } - - options_lib.read_options(opts, "subminer") - opts.socket_path = normalize_socket_path_option(opts.socket_path, default_socket_path) - return opts -end - -function M.coerce_bool(value, fallback) - if type(value) == "boolean" then - return value - end - if type(value) == "string" then - local normalized = value:lower() - if normalized == "yes" or normalized == "true" or normalized == "1" or normalized == "on" then - return true - end - if normalized == "no" or normalized == "false" or normalized == "0" or normalized == "off" then - return false - end - end - return fallback -end - -return M diff --git a/.config/mpv/scripts/subminer/process.lua b/.config/mpv/scripts/subminer/process.lua deleted file mode 100644 index 50f72cf..0000000 --- a/.config/mpv/scripts/subminer/process.lua +++ /dev/null @@ -1,542 +0,0 @@ -local M = {} - -local OVERLAY_START_RETRY_DELAY_SECONDS = 0.2 -local OVERLAY_START_MAX_ATTEMPTS = 6 -local AUTO_PLAY_READY_LOADING_OSD = "Loading subtitle tokenization..." -local AUTO_PLAY_READY_READY_OSD = "Subtitle tokenization ready" -local DEFAULT_AUTO_PLAY_READY_TIMEOUT_SECONDS = 15 - -function M.create(ctx) - local mp = ctx.mp - local opts = ctx.opts - local state = ctx.state - local binary = ctx.binary - local environment = ctx.environment - local options_helper = ctx.options_helper - local subminer_log = ctx.log.subminer_log - local show_osd = ctx.log.show_osd - local normalize_log_level = ctx.log.normalize_log_level - local run_control_command_async - - local function resolve_visible_overlay_startup() - local raw_visible_overlay = opts.auto_start_visible_overlay - if raw_visible_overlay == nil then - raw_visible_overlay = opts["auto-start-visible-overlay"] - end - return options_helper.coerce_bool(raw_visible_overlay, false) - end - - local function resolve_pause_until_ready() - local raw_pause_until_ready = opts.auto_start_pause_until_ready - if raw_pause_until_ready == nil then - raw_pause_until_ready = opts["auto-start-pause-until-ready"] - end - return options_helper.coerce_bool(raw_pause_until_ready, false) - end - - local function resolve_pause_until_ready_timeout_seconds() - local raw_timeout_seconds = opts.auto_start_pause_until_ready_timeout_seconds - if raw_timeout_seconds == nil then - raw_timeout_seconds = opts["auto-start-pause-until-ready-timeout-seconds"] - end - if type(raw_timeout_seconds) == "number" then - return raw_timeout_seconds - end - if type(raw_timeout_seconds) == "string" then - local parsed = tonumber(raw_timeout_seconds) - if parsed ~= nil then - return parsed - end - end - return DEFAULT_AUTO_PLAY_READY_TIMEOUT_SECONDS - end - - local function normalize_socket_path(path) - if type(path) ~= "string" then - return nil - end - local trimmed = path:match("^%s*(.-)%s*$") - if trimmed == "" then - return nil - end - return trimmed - end - - local function has_matching_mpv_ipc_socket(target_socket_path) - local expected_socket = normalize_socket_path(target_socket_path or opts.socket_path) - local active_socket = normalize_socket_path(mp.get_property("input-ipc-server")) - if expected_socket == nil or active_socket == nil then - return false - end - return expected_socket == active_socket - end - - local function resolve_backend(override_backend) - local selected = override_backend - if selected == nil or selected == "" then - selected = opts.backend - end - if selected == "auto" then - return environment.detect_backend() - end - return selected - end - - local function clear_auto_play_ready_timeout() - local timeout = state.auto_play_ready_timeout - if timeout and timeout.kill then - timeout:kill() - end - state.auto_play_ready_timeout = nil - end - - local function clear_auto_play_ready_osd_timer() - local timer = state.auto_play_ready_osd_timer - if timer and timer.kill then - timer:kill() - end - state.auto_play_ready_osd_timer = nil - end - - local function disarm_auto_play_ready_gate(options) - local should_resume = options == nil or options.resume_playback ~= false - local was_armed = state.auto_play_ready_gate_armed - clear_auto_play_ready_timeout() - clear_auto_play_ready_osd_timer() - state.auto_play_ready_gate_armed = false - if was_armed and should_resume then - mp.set_property_native("pause", false) - end - end - - local function release_auto_play_ready_gate(reason) - if not state.auto_play_ready_gate_armed then - return - end - disarm_auto_play_ready_gate({ resume_playback = false }) - mp.set_property_native("pause", false) - show_osd(AUTO_PLAY_READY_READY_OSD) - subminer_log("info", "process", "Resuming playback after startup gate: " .. tostring(reason or "ready")) - end - - local function arm_auto_play_ready_gate() - if state.auto_play_ready_gate_armed then - clear_auto_play_ready_timeout() - clear_auto_play_ready_osd_timer() - end - state.auto_play_ready_gate_armed = true - mp.set_property_native("pause", true) - show_osd(AUTO_PLAY_READY_LOADING_OSD) - if type(mp.add_periodic_timer) == "function" then - state.auto_play_ready_osd_timer = mp.add_periodic_timer(2.5, function() - if state.auto_play_ready_gate_armed then - show_osd(AUTO_PLAY_READY_LOADING_OSD) - end - end) - end - subminer_log("info", "process", "Pausing playback until SubMiner overlay/tokenization readiness signal") - local timeout_seconds = resolve_pause_until_ready_timeout_seconds() - if timeout_seconds and timeout_seconds > 0 then - state.auto_play_ready_timeout = mp.add_timeout(timeout_seconds, function() - if not state.auto_play_ready_gate_armed then - return - end - subminer_log( - "warn", - "process", - "Startup readiness signal timed out; resuming playback to avoid stalled pause" - ) - release_auto_play_ready_gate("timeout") - end) - end - end - - local function notify_auto_play_ready() - release_auto_play_ready_gate("tokenization-ready") - if state.suppress_ready_overlay_restore then - return - end - if state.overlay_running and resolve_visible_overlay_startup() then - run_control_command_async("show-visible-overlay", { - socket_path = opts.socket_path, - }) - end - end - - local function build_command_args(action, overrides) - overrides = overrides or {} - local args = { state.binary_path } - - table.insert(args, "--" .. action) - local log_level = normalize_log_level(overrides.log_level or opts.log_level) - if log_level ~= "info" then - table.insert(args, "--log-level") - table.insert(args, log_level) - end - - if action == "start" then - local backend = resolve_backend(overrides.backend) - if backend and backend ~= "" then - table.insert(args, "--backend") - table.insert(args, backend) - end - - local socket_path = overrides.socket_path or opts.socket_path - table.insert(args, "--socket") - table.insert(args, socket_path) - - local should_show_visible = resolve_visible_overlay_startup() - if should_show_visible then - table.insert(args, "--show-visible-overlay") - else - table.insert(args, "--hide-visible-overlay") - end - - local texthooker_enabled = overrides.texthooker_enabled - if texthooker_enabled == nil then - texthooker_enabled = opts.texthooker_enabled - end - if texthooker_enabled then - table.insert(args, "--texthooker") - end - end - - return args - end - - run_control_command_async = function(action, overrides, callback) - local args = build_command_args(action, overrides) - subminer_log("debug", "process", "Control command: " .. table.concat(args, " ")) - mp.command_native_async({ - name = "subprocess", - args = args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }, function(success, result, error) - local ok = success and (result == nil or result.status == 0) - if callback then - callback(ok, result, error) - end - end) - end - - local function parse_start_script_message_overrides(...) - local overrides = {} - for i = 1, select("#", ...) do - local token = select(i, ...) - if type(token) == "string" and token ~= "" then - local key, value = token:match("^([%w_%-]+)=(.+)$") - if key and value then - local normalized_key = key:lower() - if normalized_key == "backend" then - local backend = value:lower() - if backend == "auto" or backend == "hyprland" or backend == "sway" or backend == "x11" or backend == "macos" then - overrides.backend = backend - end - elseif normalized_key == "socket" or normalized_key == "socket_path" then - overrides.socket_path = value - elseif normalized_key == "texthooker" or normalized_key == "texthooker_enabled" then - local parsed = options_helper.coerce_bool(value, nil) - if parsed ~= nil then - overrides.texthooker_enabled = parsed - end - elseif normalized_key == "log-level" or normalized_key == "log_level" then - overrides.log_level = normalize_log_level(value) - end - end - end - end - return overrides - end - - local function ensure_texthooker_running(callback) - if callback then - callback() - end - end - - local function start_overlay(overrides) - overrides = overrides or {} - if overrides.auto_start_trigger == true then - state.suppress_ready_overlay_restore = false - end - - if not binary.ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - - if state.overlay_running then - if overrides.auto_start_trigger == true then - subminer_log("debug", "process", "Auto-start ignored because overlay is already running") - local socket_path = overrides.socket_path or opts.socket_path - local should_pause_until_ready = ( - resolve_visible_overlay_startup() - and resolve_pause_until_ready() - and has_matching_mpv_ipc_socket(socket_path) - ) - if should_pause_until_ready then - arm_auto_play_ready_gate() - else - disarm_auto_play_ready_gate() - end - local visibility_action = resolve_visible_overlay_startup() - and "show-visible-overlay" - or "hide-visible-overlay" - run_control_command_async(visibility_action, { - socket_path = socket_path, - log_level = overrides.log_level, - }) - return - end - subminer_log("info", "process", "Overlay already running") - show_osd("Already running") - return - end - - local texthooker_enabled = overrides.texthooker_enabled - if texthooker_enabled == nil then - texthooker_enabled = opts.texthooker_enabled - end - local socket_path = overrides.socket_path or opts.socket_path - local should_pause_until_ready = ( - overrides.auto_start_trigger == true - and resolve_visible_overlay_startup() - and resolve_pause_until_ready() - and has_matching_mpv_ipc_socket(socket_path) - ) - if should_pause_until_ready then - arm_auto_play_ready_gate() - else - disarm_auto_play_ready_gate() - end - - local function launch_overlay_with_retry(attempt) - local args = build_command_args("start", overrides) - if attempt == 1 then - subminer_log("info", "process", "Starting overlay: " .. table.concat(args, " ")) - else - subminer_log( - "warn", - "process", - "Retrying overlay start (attempt " .. tostring(attempt) .. "): " .. table.concat(args, " ") - ) - end - - if attempt == 1 and not state.auto_play_ready_gate_armed then - show_osd("Starting...") - end - state.overlay_running = true - - mp.command_native_async({ - name = "subprocess", - args = args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }, function(success, result, error) - if not success or (result and result.status ~= 0) then - local reason = error or (result and result.stderr) or "unknown error" - if attempt < OVERLAY_START_MAX_ATTEMPTS then - mp.add_timeout(OVERLAY_START_RETRY_DELAY_SECONDS, function() - launch_overlay_with_retry(attempt + 1) - end) - return - end - - state.overlay_running = false - subminer_log("error", "process", "Overlay start failed after retries: " .. reason) - show_osd("Overlay start failed") - release_auto_play_ready_gate("overlay-start-failed") - return - end - - if overrides.auto_start_trigger == true then - local visibility_action = resolve_visible_overlay_startup() - and "show-visible-overlay" - or "hide-visible-overlay" - run_control_command_async(visibility_action, { - socket_path = socket_path, - log_level = overrides.log_level, - }) - end - - end) - end - - launch_overlay_with_retry(1) - if texthooker_enabled then - ensure_texthooker_running(function() end) - end - end - - local function start_overlay_from_script_message(...) - local overrides = parse_start_script_message_overrides(...) - start_overlay(overrides) - end - - local function stop_overlay() - if not binary.ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - - run_control_command_async("stop", nil, function(ok, result) - if ok then - subminer_log("info", "process", "Overlay stopped") - else - subminer_log( - "warn", - "process", - "Stop command returned non-zero status: " .. tostring(result and result.status or "unknown") - ) - end - end) - - state.overlay_running = false - state.texthooker_running = false - disarm_auto_play_ready_gate() - show_osd("Stopped") - end - - local function hide_visible_overlay() - if not binary.ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - return - end - state.suppress_ready_overlay_restore = true - - run_control_command_async("hide-visible-overlay", nil, function(ok, result) - if ok then - subminer_log("info", "process", "Visible overlay hidden") - else - subminer_log( - "warn", - "process", - "Hide-visible-overlay command returned non-zero status: " - .. tostring(result and result.status or "unknown") - ) - end - end) - - disarm_auto_play_ready_gate() - end - - local function toggle_overlay() - if not binary.ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - state.suppress_ready_overlay_restore = true - - run_control_command_async("toggle-visible-overlay", nil, function(ok) - if not ok then - subminer_log("warn", "process", "Toggle command failed") - show_osd("Toggle failed") - end - end) - end - - local function open_options() - if not binary.ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - - run_control_command_async("settings", nil, function(ok) - if ok then - subminer_log("info", "process", "Options window opened") - show_osd("Options opened") - else - subminer_log("warn", "process", "Failed to open options") - show_osd("Failed to open options") - end - end) - end - - local function restart_overlay() - if not binary.ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - - subminer_log("info", "process", "Restarting overlay...") - show_osd("Restarting...") - - run_control_command_async("stop", nil, function() - state.overlay_running = false - state.texthooker_running = false - disarm_auto_play_ready_gate() - - local start_args = build_command_args("start") - subminer_log("info", "process", "Starting overlay: " .. table.concat(start_args, " ")) - - state.overlay_running = true - mp.command_native_async({ - name = "subprocess", - args = start_args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }, function(success, result, error) - if not success or (result and result.status ~= 0) then - state.overlay_running = false - subminer_log( - "error", - "process", - "Overlay start failed: " .. (error or (result and result.stderr) or "unknown error") - ) - show_osd("Restart failed") - else - show_osd("Restarted successfully") - end - end) - - if opts.texthooker_enabled then - ensure_texthooker_running(function() end) - end - end) - end - - local function check_status() - if not binary.ensure_binary_available() then - show_osd("Status: binary not found") - return - end - - local status = state.overlay_running and "running" or "stopped" - show_osd("Status: overlay is " .. status) - subminer_log("info", "process", "Status check: overlay is " .. status) - end - - local function check_binary_available() - return binary.ensure_binary_available() - end - - return { - build_command_args = build_command_args, - has_matching_mpv_ipc_socket = has_matching_mpv_ipc_socket, - run_control_command_async = run_control_command_async, - parse_start_script_message_overrides = parse_start_script_message_overrides, - ensure_texthooker_running = ensure_texthooker_running, - start_overlay = start_overlay, - start_overlay_from_script_message = start_overlay_from_script_message, - stop_overlay = stop_overlay, - hide_visible_overlay = hide_visible_overlay, - toggle_overlay = toggle_overlay, - open_options = open_options, - restart_overlay = restart_overlay, - check_status = check_status, - check_binary_available = check_binary_available, - notify_auto_play_ready = notify_auto_play_ready, - disarm_auto_play_ready_gate = disarm_auto_play_ready_gate, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/state.lua b/.config/mpv/scripts/subminer/state.lua deleted file mode 100644 index 8814b0e..0000000 --- a/.config/mpv/scripts/subminer/state.lua +++ /dev/null @@ -1,39 +0,0 @@ -local M = {} - -function M.new() - return { - overlay_running = false, - texthooker_running = false, - overlay_process = nil, - binary_available = false, - binary_path = nil, - detected_backend = nil, - hover_highlight = { - revision = -1, - payload = nil, - saved_sub_visibility = nil, - saved_secondary_sub_visibility = nil, - overlay_active = false, - cached_ass = nil, - clear_timer = nil, - last_hover_update_ts = 0, - }, - aniskip = { - mal_id = nil, - title = nil, - episode = nil, - intro_start = nil, - intro_end = nil, - payload = nil, - payload_source = nil, - found = false, - prompt_shown = false, - }, - auto_play_ready_gate_armed = false, - auto_play_ready_timeout = nil, - auto_play_ready_osd_timer = nil, - suppress_ready_overlay_restore = false, - } -end - -return M diff --git a/.config/mpv/scripts/subminer/ui.lua b/.config/mpv/scripts/subminer/ui.lua deleted file mode 100644 index f4ff0e4..0000000 --- a/.config/mpv/scripts/subminer/ui.lua +++ /dev/null @@ -1,114 +0,0 @@ -local M = {} -local DEFAULT_ANISKIP_BUTTON_KEY = "TAB" -local LEGACY_ANISKIP_BUTTON_KEY = "y-k" - -function M.create(ctx) - local mp = ctx.mp - local input = ctx.input - local opts = ctx.opts - local process = ctx.process - local aniskip = ctx.aniskip - local subminer_log = ctx.log.subminer_log - local show_osd = ctx.log.show_osd - - local function ensure_binary_for_menu() - if process.check_binary_available() then - return true - end - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return false - end - - local function show_menu() - if not ensure_binary_for_menu() then - return - end - - local items = { - "Start overlay", - "Stop overlay", - "Toggle overlay", - "Open options", - "Restart overlay", - "Check status", - "Stats", - } - - local actions = { - function() - process.start_overlay() - end, - function() - process.stop_overlay() - end, - function() - process.toggle_overlay() - end, - function() - process.open_options() - end, - function() - process.restart_overlay() - end, - function() - process.check_status() - end, - function() - mp.commandv("script-message", "subminer-stats-toggle") - end, - } - - input.select({ - prompt = "SubMiner: ", - items = items, - submit = function(index) - if index and actions[index] then - actions[index]() - end - end, - }) - end - - local function register_keybindings() - mp.add_key_binding("y-s", "subminer-start", function() - process.start_overlay() - end) - mp.add_key_binding("y-S", "subminer-stop", function() - process.stop_overlay() - end) - mp.add_key_binding("y-t", "subminer-toggle", function() - process.toggle_overlay() - end) - mp.add_key_binding("y-y", "subminer-menu", show_menu) - mp.add_key_binding("y-o", "subminer-options", function() - process.open_options() - end) - mp.add_key_binding("y-r", "subminer-restart", function() - process.restart_overlay() - end) - mp.add_key_binding("y-c", "subminer-status", function() - process.check_status() - end) - if type(opts.aniskip_button_key) == "string" and opts.aniskip_button_key ~= "" then - mp.add_key_binding(opts.aniskip_button_key, "subminer-skip-intro", function() - aniskip.skip_intro_now() - end) - end - if - opts.aniskip_button_key ~= LEGACY_ANISKIP_BUTTON_KEY - and opts.aniskip_button_key ~= DEFAULT_ANISKIP_BUTTON_KEY - then - mp.add_key_binding(LEGACY_ANISKIP_BUTTON_KEY, "subminer-skip-intro-fallback", function() - aniskip.skip_intro_now() - end) - end - end - - return { - show_menu = show_menu, - register_keybindings = register_keybindings, - } -end - -return M diff --git a/.config/mpv/scripts/ytdl-preload.lua b/.config/mpv/scripts/ytdl-preload.lua deleted file mode 120000 index d452cc0..0000000 --- a/.config/mpv/scripts/ytdl-preload.lua +++ /dev/null @@ -1 +0,0 @@ -ytdl-preload.lua##os.Linux \ No newline at end of file diff --git a/.config/mpv/scripts/ytdl-preload.lua b/.config/mpv/scripts/ytdl-preload.lua new file mode 100644 index 0000000..2e447d7 --- /dev/null +++ b/.config/mpv/scripts/ytdl-preload.lua @@ -0,0 +1,454 @@ +---------------------- +-- #example ytdl_preload.conf +-- # make sure lines do not have trailing whitespace +-- # ytdl_opt has no sanity check and should be formatted exactly how it would appear in yt-dlp CLI, they are split into a key/value pair on whitespace +-- # at least on Windows, do not escape '\' in temp, just us a single one for each divider + +-- #temp=R:\ytdltest +-- #ytdl_opt1=-r 50k +-- #ytdl_opt2=-N 5 +-- #ytdl_opt#=etc +---------------------- +local nextIndex +local caught = true +-- local pop = false +local ytdl = "yt-dlp" +local utils = require("mp.utils") + +local options = require("mp.options") +local opts = { + temp = "/tmp/ytdl-preload", + ytdl_opt1 = "", + ytdl_opt2 = "", + ytdl_opt3 = "", + ytdl_opt4 = "", + ytdl_opt5 = "", + ytdl_opt6 = "", + ytdl_opt7 = "", + ytdl_opt8 = "", + ytdl_opt9 = "", +} +options.read_options(opts, "ytdl_preload") +local additionalOpts = {} +for k, v in pairs(opts) do + if k:find("ytdl_opt%d") and v ~= "" then + additionalOpts[k] = v + -- print("entry") + -- print(k .. v) + end +end +local cachePath = opts.temp + +local chapter_list = {} +local json = "" +local filesToDelete = {} + +local function exists(file) + local ok, err, code = os.rename(file, file) + if not ok then + if code == 13 then -- Permission denied, but it exists + return true + end + end + return ok, err +end +local function useNewLoadfile() + for _, c in pairs(mp.get_property_native("command-list")) do + if c["name"] == "loadfile" then + for _, a in pairs(c["args"]) do + if a["name"] == "index" then + return true + end + end + end + end +end +--from ytdl_hook +local function time_to_secs(time_string) + local ret + local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)") + if a ~= nil then + ret = (a * 3600 + b * 60 + c) + else + a, b = time_string:match("(%d%d?):(%d%d)") + if a ~= nil then + ret = (a * 60 + b) + end + end + return ret +end +local function extract_chapters(data, video_length) + local ret = {} + for line in data:gmatch("[^\r\n]+") do + local time = time_to_secs(line) + if time and (time < video_length) then + table.insert(ret, { time = time, title = line }) + end + end + table.sort(ret, function(a, b) + return a.time < b.time + end) + return ret +end +local function chapters() + if json.chapters then + for i = 1, #json.chapters do + local chapter = json.chapters[i] + local title = chapter.title or "" + if title == "" then + title = string.format("Chapter %02d", i) + end + table.insert(chapter_list, { time = chapter.start_time, title = title }) + end + elseif not (json.description == nil) and not (json.duration == nil) then + chapter_list = extract_chapters(json.description, json.duration) + end +end +--end ytdl_hook +local title = "" +local fVideo = "" +local fAudio = "" +local function load_files(dtitle, destination, audio, wait) + if wait then + if exists(destination .. ".mka") then + print("---wait success: found mka---") + audio = "audio-file=" .. destination .. ".mka," + else + print("---could not find mka after wait, audio may be missing---") + end + end + -- if audio ~= "" then + -- table.insert(filesToDelete, destination .. ".mka") + -- end + -- table.insert(filesToDelete, destination .. ".mkv") + dtitle = dtitle:gsub("-" .. ("[%w_-]"):rep(11) .. "$", "") + dtitle = dtitle:gsub("^" .. ("%d"):rep(10) .. "%-", "") + if useNewLoadfile() then + mp.commandv( + "loadfile", + destination .. ".mkv", + "append", + -1, + audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no' + ) + else + mp.commandv( + "loadfile", + destination .. ".mkv", + "append", + audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no' + ) --,sub-file="..destination..".en.vtt") --in case they are not set up to autoload + end + mp.commandv("playlist_move", mp.get_property("playlist-count") - 1, nextIndex) + mp.commandv("playlist_remove", nextIndex + 1) + caught = true + title = "" + -- pop = true +end + +local listenID = "" +local function listener(event) + if not caught and event.prefix == mp.get_script_name() and string.find(event.text, listenID) then + local destination = string.match(event.text, "%[download%] Destination: (.+).mkv") + or string.match(event.text, "%[download%] (.+).mkv has already been downloaded") + -- if destination then print("---"..cachePath) end; + if destination and string.find(destination, string.gsub(cachePath, "~/", "")) then + -- print(listenID) + mp.unregister_event(listener) + _, title = utils.split_path(destination) + local audio = "" + if fAudio == "" then + load_files(title, destination, audio, false) + else + if exists(destination .. ".mka") then + audio = "audio-file=" .. destination .. ".mka," + load_files(title, destination, audio, false) + else + print("---expected mka but could not find it, waiting for 2 seconds---") + mp.add_timeout(2, function() + load_files(title, destination, audio, true) + end) + end + end + end + end +end + +--from ytdl_hook +mp.add_hook("on_preloaded", 10, function() + if string.find(mp.get_property("path"), cachePath) then + chapters() + if next(chapter_list) ~= nil then + mp.set_property_native("chapter-list", chapter_list) + chapter_list = {} + json = "" + end + end +end) +--end ytdl_hook +function dump(o) + if type(o) == "table" then + local s = "{ " + for k, v in pairs(o) do + if type(k) ~= "number" then + k = '"' .. k .. '"' + end + s = s .. "[" .. k .. "] = " .. dump(v) .. "," + end + return s .. "} " + else + return tostring(o) + end +end + +local function addOPTS(old) + for k, v in pairs(additionalOpts) do + -- print(k) + if string.find(v, "%s") then + for l, w in string.gmatch(v, "([-%w]+) (.+)") do + table.insert(old, l) + table.insert(old, w) + end + else + table.insert(old, v) + end + end + -- print(dump(old)) + return old +end + +local AudioDownloadHandle = {} +local VideoDownloadHandle = {} +local JsonDownloadHandle = {} +local function download_files(id, success, result, error) + if result.killed_by_us then + return + end + local jfile = cachePath .. "/" .. id .. ".json" + + local jfileIO = io.open(jfile, "w") + jfileIO:write(result.stdout) + jfileIO:close() + json = utils.parse_json(result.stdout) + -- print(dump(json)) + if json.requested_downloads[1].requested_formats ~= nil then + local args = { + ytdl, + "--no-continue", + "-q", + "-f", + fAudio, + "--restrict-filenames", + "--no-playlist", + "--no-part", + "-o", + cachePath .. "/" .. id .. "-%(title)s-%(id)s.mka", + "--load-info-json", + jfile, + } + args = addOPTS(args) + AudioDownloadHandle = mp.command_native_async({ + name = "subprocess", + args = args, + playback_only = false, + }, function() end) + else + fAudio = "" + fVideo = fVideo:gsub("bestvideo", "best") + fVideo = fVideo:gsub("bv", "best") + end + + local args = { + ytdl, + "--no-continue", + "-f", + fVideo .. "/best", + "--restrict-filenames", + "--no-playlist", + "--no-part", + "-o", + cachePath .. "/" .. id .. "-%(title)s-%(id)s.mkv", + "--load-info-json", + jfile, + } + args = addOPTS(args) + VideoDownloadHandle = mp.command_native_async({ + name = "subprocess", + args = args, + playback_only = false, + }, function() end) +end + +local function DL() + local index = tonumber(mp.get_property("playlist-pos")) + if + mp.get_property("playlist/" .. index .. "/filename"):find("/videos$") + and mp.get_property("playlist/" .. index + 1 .. "/filename"):find("/shorts$") + then + return + end + if + tonumber(mp.get_property("playlist-pos-1")) > 0 + and mp.get_property("playlist-pos-1") ~= mp.get_property("playlist-count") + then + nextIndex = index + 1 + local nextFile = mp.get_property("playlist/" .. nextIndex .. "/filename") + if nextFile and caught and nextFile:find("://", 0, false) then + caught = false + mp.enable_messages("info") + mp.register_event("log-message", listener) + local ytFormat = mp.get_property("ytdl-format") + fVideo = string.match(ytFormat, "(.+)%+.+//?") or "bestvideo" + fAudio = string.match(ytFormat, ".+%+(.+)//?") or "bestaudio" + -- print("start"..nextFile) + listenID = tostring(os.time()) + local args = { + ytdl, + "--dump-single-json", + "--no-simulate", + "--skip-download", + "--restrict-filenames", + "--no-playlist", + "--sub-lang", + "en", + "--write-sub", + "--no-part", + "-o", + cachePath .. "/" .. listenID .. "-%(title)s-%(id)s.%(ext)s", + nextFile, + } + args = addOPTS(args) + -- print(dump(args)) + table.insert(filesToDelete, listenID) + JsonDownloadHandle = mp.command_native_async({ + name = "subprocess", + args = args, + capture_stdout = true, + capture_stderr = true, + playback_only = false, + }, function(...) + download_files(listenID, ...) + end) + end + end +end + +local function clearCache() + -- print(pop) + + --if pop == true then + mp.abort_async_command(AudioDownloadHandle) + mp.abort_async_command(VideoDownloadHandle) + mp.abort_async_command(JsonDownloadHandle) + -- for k, v in pairs(filesToDelete) do + -- print("remove: " .. v) + -- os.remove(v) + -- end + local ftd = io.open(cachePath .. "/temp.files", "a") + for k, v in pairs(filesToDelete) do + ftd:write(v .. "\n") + if package.config:sub(1, 1) ~= "/" then + os.execute('del /Q /F "' .. cachePath .. "\\" .. v .. '*"') + else + os.execute("rm -f " .. cachePath .. "/" .. v .. "*") + end + end + ftd:close() + print("clear") + mp.command("quit") + --end +end +mp.add_hook("on_unload", 50, function() + -- mp.abort_async_command(AudioDownloadHandle) + -- mp.abort_async_command(VideoDownloadHandle) + mp.abort_async_command(JsonDownloadHandle) + mp.unregister_event(listener) + caught = true + listenID = "resetYtdlPreloadListener" + -- print(listenID) +end) + +local skipInitial +mp.observe_property("playlist-count", "number", function() + if skipInitial then + DL() + else + skipInitial = true + end +end) + +--from ytdl_hook +local platform_is_windows = (package.config:sub(1, 1) == "\\") +local o = { + exclude = "", + try_ytdl_first = false, + use_manifests = false, + all_formats = false, + force_all_formats = true, + ytdl_path = "", +} +local paths_to_search = { "yt-dlp", "yt-dlp_x86", "youtube-dl" } +--local options = require 'mp.options' +options.read_options(o, "ytdl_hook") + +local separator = platform_is_windows and ";" or ":" +if o.ytdl_path:match("[^" .. separator .. "]") then + paths_to_search = {} + for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do + table.insert(paths_to_search, path) + end +end + +local function exec(args) + local ret = mp.command_native({ + name = "subprocess", + args = args, + capture_stdout = true, + capture_stderr = true, + }) + return ret.status, ret.stdout, ret, ret.killed_by_us +end + +local msg = require("mp.msg") +local command = {} +for _, path in pairs(paths_to_search) do + -- search for youtube-dl in mpv's config dir + local exesuf = platform_is_windows and ".exe" or "" + local ytdl_cmd = mp.find_config_file(path .. exesuf) + if ytdl_cmd then + msg.verbose("Found youtube-dl at: " .. ytdl_cmd) + ytdl = ytdl_cmd + break + else + msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories") + --search in PATH + command[1] = path + es, json, result, aborted = exec(command) + if result.error_string == "init" then + msg.verbose("youtube-dl with path " .. path .. exesuf .. " not found in PATH or not enough permissions") + else + msg.verbose("Found youtube-dl with path " .. path .. exesuf .. " in PATH") + ytdl = path + break + end + end +end +--end ytdl_hook + +mp.register_event("start-file", DL) +mp.register_event("shutdown", clearCache) +local ftd = io.open(cachePath .. "/temp.files", "r") +while ftd ~= nil do + local line = ftd:read() + if line == nil or line == "" then + ftd:close() + io.open(cachePath .. "/temp.files", "w"):close() + break + end + -- print("DEL::"..line) + if package.config:sub(1, 1) ~= "/" then + os.execute('del /Q /F "' .. cachePath .. "\\" .. line .. '*" >nul 2>nul') + else + os.execute("rm -f " .. cachePath .. "/" .. line .. "* &> /dev/null") + end +end