mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
Revert "Use lobster as stream resolver fallback for -s query flow"
This reverts commit 022f4e972c.
This commit is contained in:
138
subminer
138
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<string, unknown>)) {
|
||||
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<ResolvedStreamTarget> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
log(
|
||||
"debug",
|
||||
args.logLevel,
|
||||
`${path.basename(args.aniCliPath || "streamer")} provided subtitle URL: ${resolvedStreamTarget.subtitleUrl}`,
|
||||
`ani-cli provided subtitle URL: ${resolvedStreamTarget.subtitleUrl}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user