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:
2026-08-01 23:34:23 -07:00
parent 590794b34e
commit 41c23b131b
25 changed files with 1721 additions and 69 deletions
+29 -4
View File
@@ -4,6 +4,7 @@ import * as path from 'path';
import * as fs from 'fs';
import * as childProcess from 'child_process';
import { createLogger } from '../logger';
import { splitSeasonFromTitle } from '../anime-bridge/episode-metadata';
import {
JimakuApiResponse,
JimakuConfig,
@@ -163,6 +164,28 @@ function matchEpisodeFromName(name: string): {
};
}
// Spelled-out labels. Streaming sources name episodes this way ("Episode 4"),
// and the abbreviated `E04` pattern below cannot see them.
const worded = name.match(/(?:^|[\s._-])episode\s*[.#]?\s*(\d{1,3})(?:\D|$)/i);
if (worded && worded.index !== undefined) {
return {
season: null,
episode: Number.parseInt(worded[1]!, 10),
index: worded.index,
confidence: 'medium',
};
}
const japanese = name.match(/第\s*(\d{1,3})\s*話/);
if (japanese && japanese.index !== undefined) {
return {
season: null,
episode: Number.parseInt(japanese[1]!, 10),
index: japanese.index,
confidence: 'medium',
};
}
const epOnly = name.match(/(?:^|[\s._-])E(?:P)?(\d{1,3})(?:\b|[\s._-])/i);
if (epOnly && epOnly.index !== undefined) {
return {
@@ -229,12 +252,14 @@ export function parseMediaInfo(mediaPath: string | null): JimakuMediaInfo {
titlePart = name.slice(0, parsed.index);
}
const seasonFromDir = parsed.season ?? detectSeasonFromDir(normalizedMediaPath);
const title = cleanupTitle(titlePart || name);
// A season named in the title ("… Season 3") belongs in the season field, not
// glued to the title — otherwise it is searched for as part of the name.
const titleSeason = splitSeasonFromTitle(cleanupTitle(titlePart || name));
const season = parsed.season ?? titleSeason.season ?? detectSeasonFromDir(normalizedMediaPath);
return {
title,
season: seasonFromDir,
title: titleSeason.title,
season,
episode: parsed.episode,
confidence: parsed.confidence,
filename,