mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-02 19:21:34 -07:00
fix(anime): thread episode metadata through stream playback and fix seas
- Split anime browser stream titles into series/season/episode fields (episode-metadata.ts) instead of one joined string, so stats grouping, the mpv title, and the Jimaku/TsukiHime modals all get real values instead of a filename-derived "m3u8" - Fix AniList season resolver to walk through split-cour sequels and specials without counting them as a season, and to surface rate-limit/network failures instead of falling through to a wrong air-order guess - Add a Season field to the TsukiHime modal and include later seasons in its search query - Rewrite WebVTT subtitle tracks as SRT before handing them to alass, since alass has no VTT support and streams serve VTT - Update CHANGELOG and troubleshooting docs
This commit is contained in:
@@ -4313,3 +4313,116 @@ test('ensureAnimeCoverArt fetches art via the latest video of the anime', async
|
||||
cleanupDbPath(dbPath);
|
||||
}
|
||||
});
|
||||
|
||||
test('anime browser streams group by series instead of by the proxy file extension', async () => {
|
||||
const dbPath = makeDbPath();
|
||||
let tracker: ImmersionTrackerService | null = null;
|
||||
|
||||
try {
|
||||
const Ctor = await loadTrackerCtor();
|
||||
tracker = new Ctor({ dbPath });
|
||||
|
||||
// Two different shows, each behind the strip proxy. Without the metadata
|
||||
// recorded up front the only readable part of either URL is ".m3u8", which
|
||||
// is what used to collapse them into one series.
|
||||
tracker.recordStreamPlaybackMetadata({
|
||||
mediaPath: 'http://127.0.0.1:41234/video/9f2c1b7a.m3u8',
|
||||
statsPath: 'animebrowser://9001/%2Fanime%2Fmushoku/%2Fwatch%2Fep-4',
|
||||
displayTitle: 'Mushoku Tensei: Jobless Reincarnation S03E04',
|
||||
seriesTitle: 'Mushoku Tensei: Jobless Reincarnation',
|
||||
seasonNumber: 3,
|
||||
episodeNumber: 4,
|
||||
});
|
||||
tracker.handleMediaChange(
|
||||
'http://127.0.0.1:41234/video/9f2c1b7a.m3u8',
|
||||
'Mushoku Tensei: Jobless Reincarnation S03E04',
|
||||
);
|
||||
await waitForPendingAnimeMetadata(tracker);
|
||||
tracker.handleMediaChange(null, null);
|
||||
|
||||
tracker.recordStreamPlaybackMetadata({
|
||||
mediaPath: 'http://127.0.0.1:41234/video/33ab90ff.m3u8',
|
||||
statsPath: 'animebrowser://9001/%2Fanime%2Fsnafu/%2Fwatch%2Fep-10',
|
||||
displayTitle: 'My Teen Romantic Comedy SNAFU Climax! E10 - Gallantly',
|
||||
seriesTitle: 'My Teen Romantic Comedy SNAFU Climax!',
|
||||
seasonNumber: null,
|
||||
episodeNumber: 10,
|
||||
});
|
||||
tracker.handleMediaChange(
|
||||
'http://127.0.0.1:41234/video/33ab90ff.m3u8',
|
||||
'My Teen Romantic Comedy SNAFU Climax! E10 - Gallantly',
|
||||
);
|
||||
await waitForPendingAnimeMetadata(tracker);
|
||||
tracker.handleMediaChange(null, null);
|
||||
|
||||
// The same episode again: a fresh proxy port and token, which must not mint
|
||||
// a second video row.
|
||||
tracker.recordStreamPlaybackMetadata({
|
||||
mediaPath: 'http://127.0.0.1:55555/video/ffff0000.m3u8',
|
||||
statsPath: 'animebrowser://9001/%2Fanime%2Fmushoku/%2Fwatch%2Fep-4',
|
||||
displayTitle: 'Mushoku Tensei: Jobless Reincarnation S03E04',
|
||||
seriesTitle: 'Mushoku Tensei: Jobless Reincarnation',
|
||||
seasonNumber: 3,
|
||||
episodeNumber: 4,
|
||||
});
|
||||
tracker.handleMediaChange(
|
||||
'http://127.0.0.1:55555/video/ffff0000.m3u8',
|
||||
'Mushoku Tensei: Jobless Reincarnation S03E04',
|
||||
);
|
||||
await waitForPendingAnimeMetadata(tracker);
|
||||
|
||||
const privateApi = tracker as unknown as { db: DatabaseSync };
|
||||
const rows = privateApi.db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
v.source_url,
|
||||
v.canonical_title AS video_title,
|
||||
v.parsed_title,
|
||||
v.parsed_season,
|
||||
v.parsed_episode,
|
||||
v.parser_source,
|
||||
a.canonical_title AS anime_title
|
||||
FROM imm_videos v
|
||||
JOIN imm_anime a ON a.anime_id = v.anime_id
|
||||
ORDER BY v.video_id
|
||||
`,
|
||||
)
|
||||
.all() as Array<{
|
||||
source_url: string | null;
|
||||
video_title: string;
|
||||
parsed_title: string | null;
|
||||
parsed_season: number | null;
|
||||
parsed_episode: number | null;
|
||||
parser_source: string | null;
|
||||
anime_title: string;
|
||||
}>;
|
||||
|
||||
// Two episodes, three playbacks: the rewatch reused its row.
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(new Set(rows.map((row) => row.anime_title)).size, 2);
|
||||
assert.equal(
|
||||
rows.some((row) => row.anime_title.toLowerCase().includes('m3u8')),
|
||||
false,
|
||||
);
|
||||
|
||||
const mushoku = rows.find((row) => row.parsed_title?.startsWith('Mushoku'));
|
||||
assert.ok(mushoku);
|
||||
assert.equal(mushoku.source_url, 'animebrowser://9001/%2Fanime%2Fmushoku/%2Fwatch%2Fep-4');
|
||||
assert.equal(mushoku.video_title, 'Mushoku Tensei: Jobless Reincarnation S03E04');
|
||||
assert.equal(mushoku.parsed_title, 'Mushoku Tensei: Jobless Reincarnation');
|
||||
assert.equal(mushoku.parsed_season, 3);
|
||||
assert.equal(mushoku.parsed_episode, 4);
|
||||
assert.equal(mushoku.parser_source, 'anime-browser');
|
||||
assert.equal(mushoku.anime_title, 'Mushoku Tensei: Jobless Reincarnation Season 3');
|
||||
|
||||
const snafu = rows.find((row) => row.parsed_title?.startsWith('My Teen'));
|
||||
assert.ok(snafu);
|
||||
assert.equal(snafu.parsed_episode, 10);
|
||||
assert.equal(snafu.parsed_season, null);
|
||||
assert.equal(snafu.anime_title, 'My Teen Romantic Comedy SNAFU Climax!');
|
||||
} finally {
|
||||
tracker?.destroy();
|
||||
cleanupDbPath(dbPath);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user