mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-01 07:21:33 -07:00
ff25e5cafa
Some hosts prepend a fake image header (a real 1x1 PNG) to every HLS segment; ffmpeg probes the segment as a picture and mpv drops back to idle with no window while the browser claims the episode is playing. - Route bridge-served m3u8 streams through a local strip proxy that scans each segment for the first genuine MPEG-TS packet run and drops the junk in front of it; playlists get absolute origins rewritten so segment requests come back through the proxy. Non-TS bodies pass through untouched. - Only report ok from playEpisode once mpv configures a video output; an end-file with reason error surfaces mpv's own message (e.g. "no audio or video data played") in the browser status bar instead of "Playing". New end-file event plumbed through the mpv IPC client.
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
/**
|
|
* The bridge returns cover art and video URLs pointing at its own loopback
|
|
* media proxy, but the origin it embeds is not always the port we actually
|
|
* started it on. Rebase those onto the live bridge origin, and leave any
|
|
* genuinely remote URL untouched.
|
|
*/
|
|
|
|
const PROXY_ROUTES = new Set(['image', 'video']);
|
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
|
|
function isLoopbackProxyUrl(candidate: URL): boolean {
|
|
if (candidate.protocol !== 'http:' && candidate.protocol !== 'https:') return false;
|
|
const host = candidate.hostname.toLowerCase();
|
|
if (!LOOPBACK_HOSTS.has(host)) return false;
|
|
const route = candidate.pathname.split('/').filter(Boolean)[0];
|
|
return route !== undefined && PROXY_ROUTES.has(route);
|
|
}
|
|
|
|
/**
|
|
* Rewrite a bridge media URL onto `bridgeBaseUrl`, preserving path and query.
|
|
* Returns the input unchanged when it is not a loopback proxy URL, or when
|
|
* either URL cannot be parsed.
|
|
*/
|
|
export function resolveBridgeMediaUrl(bridgeBaseUrl: string, mediaUrl: string): string {
|
|
let media: URL;
|
|
try {
|
|
media = new URL(mediaUrl);
|
|
} catch {
|
|
return mediaUrl;
|
|
}
|
|
if (!isLoopbackProxyUrl(media)) return mediaUrl;
|
|
|
|
const normalizedBase = bridgeBaseUrl.includes('://') ? bridgeBaseUrl : `http://${bridgeBaseUrl}`;
|
|
let base: URL;
|
|
try {
|
|
base = new URL(normalizedBase);
|
|
} catch {
|
|
return mediaUrl;
|
|
}
|
|
if (base.protocol !== 'http:' && base.protocol !== 'https:') return mediaUrl;
|
|
if (base.hostname.length === 0) return mediaUrl;
|
|
|
|
const rebased = new URL(base.origin);
|
|
rebased.pathname = media.pathname;
|
|
rebased.search = media.search;
|
|
rebased.hash = media.hash;
|
|
return rebased.toString();
|
|
}
|
|
|
|
/**
|
|
* Send a bridge-served HLS stream through the local strip proxy instead. Only
|
|
* `.m3u8` URLs on the bridge origin qualify: direct files need no fixing, and
|
|
* an external URL would not resolve through a proxy that forwards to the
|
|
* bridge. Anything unparseable comes back unchanged.
|
|
*/
|
|
export function routeHlsThroughProxy(
|
|
streamUrl: string,
|
|
bridgeBaseUrl: string,
|
|
proxyOrigin: string,
|
|
): string {
|
|
let stream: URL;
|
|
let bridge: URL;
|
|
try {
|
|
stream = new URL(streamUrl);
|
|
bridge = new URL(bridgeBaseUrl);
|
|
} catch {
|
|
return streamUrl;
|
|
}
|
|
if (stream.origin !== bridge.origin) return streamUrl;
|
|
if (!stream.pathname.endsWith('.m3u8')) return streamUrl;
|
|
return `${proxyOrigin}${stream.pathname}${stream.search}`;
|
|
}
|
|
|
|
/** Aniyomi's SAnime status constants. */
|
|
export type AnimeStatus =
|
|
| 'unknown'
|
|
| 'ongoing'
|
|
| 'completed'
|
|
| 'publishing-finished'
|
|
| 'cancelled'
|
|
| 'on-hiatus';
|
|
|
|
export function parseAnimeStatus(status: number | undefined): AnimeStatus {
|
|
switch (status) {
|
|
case 1:
|
|
return 'ongoing';
|
|
case 2:
|
|
return 'completed';
|
|
case 4:
|
|
return 'publishing-finished';
|
|
case 5:
|
|
return 'cancelled';
|
|
case 6:
|
|
return 'on-hiatus';
|
|
default:
|
|
return 'unknown';
|
|
}
|
|
}
|