From 3dfa713b29afb91d37175734329a6fd6ce779c6d Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 13 Feb 2026 22:58:37 -0800 Subject: [PATCH] Revert "Use lobster as stream resolver fallback for -s query flow" This reverts commit 022f4e972c1c923d2ced938c7cccda47a1cb05e2. --- subminer | 138 ++++++++++--------------------------------------------- 1 file changed, 25 insertions(+), 113 deletions(-) diff --git a/subminer b/subminer index 3337ceb..d346e7f 100755 --- a/subminer +++ b/subminer @@ -609,7 +609,7 @@ Options: -d, --directory DIR Directory to browse for videos (default: current directory) -r, --recursive Search for videos recursively -p, --profile PROFILE MPV profile to use (default: subminer) - -s, --stream Resolve stream URL via ani-cli/lobster using the given query + -s, --stream Resolve stream URL via ani-cli using the given query --start Explicitly start SubMiner overlay --yt-subgen-mode MODE YouTube subtitle generation mode: automatic, preprocess, off (default: automatic) @@ -633,7 +633,6 @@ Options: Environment: SUBMINER_APPIMAGE_PATH Path to SubMiner AppImage/binary (optional override) SUBMINER_ROFI_THEME Path to rofi theme file (optional override) - SUBMINER_STREAM_TOOL_PATH Path override for stream resolver binary (ani-cli or lobster) SUBMINER_YT_SUBGEN_MODE automatic, preprocess, off (optional default) SUBMINER_WHISPER_BIN whisper.cpp binary path (optional fallback) SUBMINER_WHISPER_MODEL whisper model path (optional fallback) @@ -647,7 +646,7 @@ Examples: ${scriptName} video.mkv # Play specific file ${scriptName} https://youtu.be/... # Play a YouTube URL ${scriptName} ytsearch:query # Play first YouTube search result - ${scriptName} -s \"blue lock\" # Resolve and play Blue Lock stream via ani-cli/lobster + ${scriptName} -s \"blue lock\" # Resolve and play Blue Lock stream via ani-cli ${scriptName} --yt-subgen-mode preprocess --whisper-bin /path/whisper-cli --whisper-model /path/model.bin https://youtu.be/... ${scriptName} video.mkv # Play with subminer profile ${scriptName} -p gpu-hq video.mkv # Play with gpu-hq profile @@ -1680,19 +1679,16 @@ function findAppBinary(selfPath: string): string | null { } function resolveAniCliPath(scriptPath: string): string | null { - const envPath = - process.env.SUBMINER_STREAM_TOOL_PATH || process.env.SUBMINER_ANI_CLI_PATH; + const envPath = process.env.SUBMINER_ANI_CLI_PATH; if (envPath && isExecutable(envPath)) return envPath; const candidates: string[] = []; candidates.push(path.resolve(path.dirname(realpathMaybe(scriptPath)), "ani-cli/ani-cli")); - candidates.push(path.resolve(path.dirname(realpathMaybe(scriptPath)), "lobster/lobster.sh")); for (const candidate of candidates) { if (isExecutable(candidate)) return candidate; } - if (commandExists("lobster")) return "lobster"; if (commandExists("ani-cli")) return "ani-cli"; return null; @@ -1715,11 +1711,11 @@ function buildAniCliRofiConfig( } catch (error) { fs.rmSync(tempDir, { force: true, recursive: true }); throw new Error( - `Failed to prepare temporary stream resolver rofi theme config: ${(error as Error).message}`, + `Failed to prepare temporary ani-cli rofi theme config: ${(error as Error).message}`, ); } - log("debug", logLevel, `Using Subminer rofi theme for stream resolver via ${configPath}`); + log("debug", logLevel, `Using Subminer rofi theme for ani-cli via ${configPath}`); return { env: { @@ -1731,84 +1727,7 @@ function buildAniCliRofiConfig( }; } -function extractUrls(value: unknown, out: string[] = []): string[] { - if (typeof value === "string") { - if (/^https?:\/\//i.test(value)) out.push(value); - return out; - } - if (Array.isArray(value)) { - for (const item of value) extractUrls(item, out); - return out; - } - if (value && typeof value === "object") { - for (const item of Object.values(value as Record)) { - extractUrls(item, out); - } - } - return out; -} - -function extractFirstJsonObject(text: string): string | null { - let depth = 0; - let start = -1; - - for (let i = 0; i < text.length; i++) { - const char = text[i]; - if (char === "{") { - if (depth === 0) start = i; - depth += 1; - } else if (char === "}") { - if (depth > 0) depth -= 1; - if (depth === 0 && start >= 0) return text.slice(start, i + 1); - } - } - - return null; -} - -function pickStreamUrl(urls: string[]): string | null { - const prioritized = urls.filter((url) => { - const lower = url.toLowerCase(); - return lower.includes(".m3u8") || lower.includes(".mp4") || lower.includes("/playlist"); - }); - return ( - prioritized[0] ?? - urls[0] ?? - null - ); -} - -function pickSubtitleUrl(urls: string[], streamUrl: string): string | undefined { - const candidates = urls.filter((url) => url !== streamUrl); - const explicit = candidates.find((url) => { - const lower = url.toLowerCase(); - return lower.endsWith(".vtt") || lower.endsWith(".srt") || lower.endsWith(".ass") || - lower.endsWith(".ssa") || lower.includes("subtitle"); - }); - return explicit ?? candidates[0]; -} - function parseAniCliOutput(output: string): ResolvedStreamTarget { - const trimmedOutput = output.trim(); - if (trimmedOutput) { - const jsonBlock = extractFirstJsonObject(trimmedOutput); - if (jsonBlock) { - try { - const parsed = JSON.parse(jsonBlock); - const urls = [...new Set(extractUrls(parsed))]; - const streamUrl = pickStreamUrl(urls); - if (streamUrl) { - return { - streamUrl, - subtitleUrl: pickSubtitleUrl(urls, streamUrl), - }; - } - } catch { - // fall back to legacy text parsing - } - } - } - const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); const selectedIndex = lines.findIndex((line) => line.startsWith("Selected link:"), @@ -1817,14 +1736,16 @@ function parseAniCliOutput(output: string): ResolvedStreamTarget { selectedIndex >= 0 ? lines.slice(selectedIndex + 1) : lines.slice(); - const urls = selectedBlock - .flatMap((line) => line.match(/https?:\/\/\S+/g) ?? []); - const targetCandidate = pickStreamUrl(urls); + const targetCandidate = selectedBlock + .flatMap((line) => line.match(/https?:\/\/\S+/g) ?? []) + .find((value) => value.length > 0); if (!targetCandidate) { - throw new Error("Could not parse stream URL from output."); + throw new Error("Could not parse ani-cli stream URL from output."); } - const subtitleCandidate = pickSubtitleUrl(urls, targetCandidate); + const subtitleCandidate = lines + .find((line) => line.startsWith("subtitle >") || line.includes("subtitle >")) + ?.match(/https?:\/\/\S+/)?.[0]; return { streamUrl: targetCandidate, @@ -1838,31 +1759,22 @@ async function resolveStreamTarget( scriptPath: string, ): Promise { const aniCliThemeConfig = buildAniCliRofiConfig(scriptPath, args.logLevel); - const isLobster = args.aniCliPath - ? path.basename(args.aniCliPath) === "lobster.sh" - : false; - const commandArgs = isLobster ? ["-j", query] : [query]; - try { - const result = await runExternalCommand( - args.aniCliPath as string, - commandArgs, - { - captureStdout: true, - logLevel: args.logLevel, - commandLabel: isLobster ? "lobster" : "ani-cli", - streamOutput: false, - env: { - ANI_CLI_PLAYER: "debug", - ...aniCliThemeConfig?.env, - }, + const result = await runExternalCommand(args.aniCliPath as string, [query], { + captureStdout: true, + logLevel: args.logLevel, + commandLabel: "ani-cli", + streamOutput: false, + env: { + ANI_CLI_PLAYER: "debug", + ...aniCliThemeConfig?.env, }, - ); + }); const parsed = parseAniCliOutput(result.stdout); if (!parsed.streamUrl.startsWith("http://") && !parsed.streamUrl.startsWith("https://")) { throw new Error( - `Stream resolver output URL is invalid: ${parsed.streamUrl}`, + `Ani-cli output stream URL is invalid: ${parsed.streamUrl}`, ); } log("info", args.logLevel, `Resolved stream target: ${parsed.streamUrl}`); @@ -2012,7 +1924,7 @@ function checkDependencies(args: Args): void { if (!commandExists("mpv")) missing.push("mpv"); if (args.streamMode) { - if (!args.aniCliPath) missing.push("lobster or ani-cli"); + if (!args.aniCliPath) missing.push("ani-cli"); } if ( @@ -3090,7 +3002,7 @@ async function main(): Promise { const streamSource = selectedTarget.target; if (args.streamMode) { - log("info", args.logLevel, `Resolving stream target via streamer for "${selectedTarget.target}"`); + log("info", args.logLevel, `Resolving stream target via ani-cli for "${selectedTarget.target}"`); resolvedStreamTarget = await resolveStreamTarget( selectedTarget.target, args, @@ -3104,7 +3016,7 @@ async function main(): Promise { log( "debug", args.logLevel, - `${path.basename(args.aniCliPath || "streamer")} provided subtitle URL: ${resolvedStreamTarget.subtitleUrl}`, + `ani-cli provided subtitle URL: ${resolvedStreamTarget.subtitleUrl}`, ); } }