fix(anime): keep browser state in sync and alias HLS segments

- Toggle the player Anime Browser without losing its state
- Share active playback state and support rotating fake segment extensions
This commit is contained in:
2026-08-15 23:35:37 -07:00
parent 7a1450ddb7
commit 877f350353
34 changed files with 612 additions and 43 deletions
+6
View File
@@ -10,6 +10,9 @@
/** Where a stream came from, resolved into the fields consumers actually want. */
export interface AnimeStreamMetadata {
sourceId: string;
animeUrl: string;
episodeUrl: string;
/** The URL handed to mpv. Matches what mpv reports as `path`. */
mediaPath: string;
/**
@@ -192,6 +195,9 @@ export function buildAnimeStreamMetadata(input: AnimeStreamMetadataInput): Anime
const displayTitle = buildStreamDisplayTitle(seriesTitle, season, episodeNumber, label.title);
return {
sourceId: input.sourceId,
animeUrl: input.animeUrl,
episodeUrl: input.episodeUrl,
mediaPath: input.mediaPath,
statsPath: buildAnimeStreamStatsPath(input.sourceId, input.animeUrl, input.episodeUrl),
seriesTitle,
+28 -1
View File
@@ -61,7 +61,7 @@ test('rewritePlaylistOrigins swaps absolute upstream URLs and keeps relative lin
assert.ok(!rewritten.includes('41569'));
});
test('rewritePlaylistOrigins gives proxied image segments an ffmpeg-safe TS suffix', () => {
test('rewritePlaylistOrigins gives proxied disguised segments an ffmpeg-safe TS suffix', () => {
const body = [
'#EXTM3U',
'#EXTINF:6.006,',
@@ -70,6 +70,14 @@ test('rewritePlaylistOrigins gives proxied image segments an ffmpeg-safe TS suff
'http://127.0.0.1:41569/video/absolute.image',
'#EXTINF:3,',
'https://cdn.example/video/external.image',
'#EXTINF:2,',
'/video/rotating.jpg',
'#EXTINF:2,',
'/video/rotating.css',
'#EXTINF:2,',
'/video/rotating.html',
'#EXTINF:2,',
'/video/extensionless',
].join('\n');
const rewritten = rewritePlaylistOrigins(body, 'http://127.0.0.1:41569', 'http://127.0.0.1:9999');
@@ -78,6 +86,25 @@ test('rewritePlaylistOrigins gives proxied image segments an ffmpeg-safe TS suff
rewritten.includes(`http://127.0.0.1:9999/video/absolute.image${TS_SEGMENT_ALIAS_SUFFIX}`),
);
assert.ok(rewritten.includes('https://cdn.example/video/external.image'));
assert.ok(rewritten.includes(`/video/rotating.jpg${TS_SEGMENT_ALIAS_SUFFIX}`));
assert.ok(rewritten.includes(`/video/rotating.css${TS_SEGMENT_ALIAS_SUFFIX}`));
assert.ok(rewritten.includes(`/video/rotating.html${TS_SEGMENT_ALIAS_SUFFIX}`));
assert.ok(rewritten.includes(`/video/extensionless${TS_SEGMENT_ALIAS_SUFFIX}`));
});
test('rewritePlaylistOrigins leaves recognized media extensions unaliased', () => {
const body = [
'#EXTM3U',
'#EXTINF:6,',
'/video/plain.ts',
'#EXTINF:6,',
'/video/fragmented.m4s',
'#EXTINF:6,',
'/subs/line.vtt',
'/video/nested.m3u8',
].join('\n');
const rewritten = rewritePlaylistOrigins(body, 'http://127.0.0.1:41569', 'http://127.0.0.1:9999');
assert.ok(!rewritten.includes(TS_SEGMENT_ALIAS_SUFFIX));
});
/* ---------- proxy end-to-end ---------- */
+47 -3
View File
@@ -25,10 +25,54 @@ const TS_SYNC_BYTE = 0x47;
const SYNC_RUN = 5;
/**
* FFmpeg 8.1 rejects HLS media whose URL suffix is not in its segment allowlist.
* The disguised MPEG-TS segments seen in the wild use `.image`, so the local
* playlist gives them this safe alias and removes it again before forwarding.
* Hosts disguise MPEG-TS segments behind rotating fake extensions (`.image`,
* `.jpg`, `.css`, ...), so the local playlist gives every proxied segment
* without a recognized media extension this safe alias and removes it again
* before forwarding.
*/
export const TS_SEGMENT_ALIAS_SUFFIX = '.subminer.ts';
/** ffmpeg 8.1 hls demuxer `allowed_segment_extensions` defaults (minus `html`,
* which only newer builds accept and is a disguise whenever it shows up here). */
const FFMPEG_SAFE_SEGMENT_EXTENSIONS = new Set([
'3gp',
'aac',
'avi',
'ac3',
'eac3',
'flac',
'mkv',
'm3u8',
'm4a',
'm4s',
'm4v',
'mpg',
'mov',
'mp2',
'mp3',
'mp4',
'mpeg',
'mpegts',
'ogg',
'ogv',
'oga',
'ts',
'vob',
'vtt',
'wav',
'webvtt',
'cmfv',
'cmfa',
'ec3',
'fmp4',
]);
/** True when ffmpeg's picky segment-extension check would reject this path. */
function needsTsSegmentAlias(pathname: string): boolean {
const name = pathname.slice(pathname.lastIndexOf('/') + 1).toLowerCase();
const dot = name.lastIndexOf('.');
if (dot === -1) return true;
return !FFMPEG_SAFE_SEGMENT_EXTENSIONS.has(name.slice(dot + 1));
}
/** A disguise prefix is small; give up scanning after this much. */
export const DEFAULT_SCAN_LIMIT_BYTES = 1024 * 1024;
/** Bytes needed to either find a run within the limit or rule one out. */
@@ -80,7 +124,7 @@ export function rewritePlaylistOrigins(
} catch {
return line;
}
if (resolved.origin !== proxyOrigin || !resolved.pathname.toLowerCase().endsWith('.image')) {
if (resolved.origin !== proxyOrigin || !needsTsSegmentAlias(resolved.pathname)) {
return line;
}