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)
|
-d, --directory DIR Directory to browse for videos (default: current directory)
|
||||||
-r, --recursive Search for videos recursively
|
-r, --recursive Search for videos recursively
|
||||||
-p, --profile PROFILE MPV profile to use (default: subminer)
|
-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
|
--start Explicitly start SubMiner overlay
|
||||||
--yt-subgen-mode MODE
|
--yt-subgen-mode MODE
|
||||||
YouTube subtitle generation mode: automatic, preprocess, off (default: automatic)
|
YouTube subtitle generation mode: automatic, preprocess, off (default: automatic)
|
||||||
@@ -633,7 +633,6 @@ Options:
|
|||||||
Environment:
|
Environment:
|
||||||
SUBMINER_APPIMAGE_PATH Path to SubMiner AppImage/binary (optional override)
|
SUBMINER_APPIMAGE_PATH Path to SubMiner AppImage/binary (optional override)
|
||||||
SUBMINER_ROFI_THEME Path to rofi theme file (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_YT_SUBGEN_MODE automatic, preprocess, off (optional default)
|
||||||
SUBMINER_WHISPER_BIN whisper.cpp binary path (optional fallback)
|
SUBMINER_WHISPER_BIN whisper.cpp binary path (optional fallback)
|
||||||
SUBMINER_WHISPER_MODEL whisper model path (optional fallback)
|
SUBMINER_WHISPER_MODEL whisper model path (optional fallback)
|
||||||
@@ -647,7 +646,7 @@ Examples:
|
|||||||
${scriptName} video.mkv # Play specific file
|
${scriptName} video.mkv # Play specific file
|
||||||
${scriptName} https://youtu.be/... # Play a YouTube URL
|
${scriptName} https://youtu.be/... # Play a YouTube URL
|
||||||
${scriptName} ytsearch:query # Play first YouTube search result
|
${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} --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} video.mkv # Play with subminer profile
|
||||||
${scriptName} -p gpu-hq video.mkv # Play with gpu-hq 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 {
|
function resolveAniCliPath(scriptPath: string): string | null {
|
||||||
const envPath =
|
const envPath = process.env.SUBMINER_ANI_CLI_PATH;
|
||||||
process.env.SUBMINER_STREAM_TOOL_PATH || process.env.SUBMINER_ANI_CLI_PATH;
|
|
||||||
if (envPath && isExecutable(envPath)) return envPath;
|
if (envPath && isExecutable(envPath)) return envPath;
|
||||||
|
|
||||||
const candidates: string[] = [];
|
const candidates: string[] = [];
|
||||||
candidates.push(path.resolve(path.dirname(realpathMaybe(scriptPath)), "ani-cli/ani-cli"));
|
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) {
|
for (const candidate of candidates) {
|
||||||
if (isExecutable(candidate)) return candidate;
|
if (isExecutable(candidate)) return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandExists("lobster")) return "lobster";
|
|
||||||
if (commandExists("ani-cli")) return "ani-cli";
|
if (commandExists("ani-cli")) return "ani-cli";
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -1715,11 +1711,11 @@ function buildAniCliRofiConfig(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||||
throw new Error(
|
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 {
|
return {
|
||||||
env: {
|
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 {
|
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 lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||||
const selectedIndex = lines.findIndex((line) =>
|
const selectedIndex = lines.findIndex((line) =>
|
||||||
line.startsWith("Selected link:"),
|
line.startsWith("Selected link:"),
|
||||||
@@ -1817,14 +1736,16 @@ function parseAniCliOutput(output: string): ResolvedStreamTarget {
|
|||||||
selectedIndex >= 0
|
selectedIndex >= 0
|
||||||
? lines.slice(selectedIndex + 1)
|
? lines.slice(selectedIndex + 1)
|
||||||
: lines.slice();
|
: lines.slice();
|
||||||
const urls = selectedBlock
|
const targetCandidate = selectedBlock
|
||||||
.flatMap((line) => line.match(/https?:\/\/\S+/g) ?? []);
|
.flatMap((line) => line.match(/https?:\/\/\S+/g) ?? [])
|
||||||
const targetCandidate = pickStreamUrl(urls);
|
.find((value) => value.length > 0);
|
||||||
if (!targetCandidate) {
|
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 {
|
return {
|
||||||
streamUrl: targetCandidate,
|
streamUrl: targetCandidate,
|
||||||
@@ -1838,31 +1759,22 @@ async function resolveStreamTarget(
|
|||||||
scriptPath: string,
|
scriptPath: string,
|
||||||
): Promise<ResolvedStreamTarget> {
|
): Promise<ResolvedStreamTarget> {
|
||||||
const aniCliThemeConfig = buildAniCliRofiConfig(scriptPath, args.logLevel);
|
const aniCliThemeConfig = buildAniCliRofiConfig(scriptPath, args.logLevel);
|
||||||
const isLobster = args.aniCliPath
|
|
||||||
? path.basename(args.aniCliPath) === "lobster.sh"
|
|
||||||
: false;
|
|
||||||
const commandArgs = isLobster ? ["-j", query] : [query];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runExternalCommand(
|
const result = await runExternalCommand(args.aniCliPath as string, [query], {
|
||||||
args.aniCliPath as string,
|
captureStdout: true,
|
||||||
commandArgs,
|
logLevel: args.logLevel,
|
||||||
{
|
commandLabel: "ani-cli",
|
||||||
captureStdout: true,
|
streamOutput: false,
|
||||||
logLevel: args.logLevel,
|
env: {
|
||||||
commandLabel: isLobster ? "lobster" : "ani-cli",
|
ANI_CLI_PLAYER: "debug",
|
||||||
streamOutput: false,
|
...aniCliThemeConfig?.env,
|
||||||
env: {
|
|
||||||
ANI_CLI_PLAYER: "debug",
|
|
||||||
...aniCliThemeConfig?.env,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const parsed = parseAniCliOutput(result.stdout);
|
const parsed = parseAniCliOutput(result.stdout);
|
||||||
if (!parsed.streamUrl.startsWith("http://") && !parsed.streamUrl.startsWith("https://")) {
|
if (!parsed.streamUrl.startsWith("http://") && !parsed.streamUrl.startsWith("https://")) {
|
||||||
throw new Error(
|
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}`);
|
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 (!commandExists("mpv")) missing.push("mpv");
|
||||||
|
|
||||||
if (args.streamMode) {
|
if (args.streamMode) {
|
||||||
if (!args.aniCliPath) missing.push("lobster or ani-cli");
|
if (!args.aniCliPath) missing.push("ani-cli");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -3090,7 +3002,7 @@ async function main(): Promise<void> {
|
|||||||
const streamSource = selectedTarget.target;
|
const streamSource = selectedTarget.target;
|
||||||
|
|
||||||
if (args.streamMode) {
|
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(
|
resolvedStreamTarget = await resolveStreamTarget(
|
||||||
selectedTarget.target,
|
selectedTarget.target,
|
||||||
args,
|
args,
|
||||||
@@ -3104,7 +3016,7 @@ async function main(): Promise<void> {
|
|||||||
log(
|
log(
|
||||||
"debug",
|
"debug",
|
||||||
args.logLevel,
|
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