From 473c82ec0ab3a73a6d7b8aa95f3c10baec02dbf5 Mon Sep 17 00:00:00 2001 From: foxyz Date: Sat, 10 Oct 2026 20:42:28 +0300 Subject: [PATCH] Schedule: AnimeFlow's AniList episode number wins outright (JoJo S6E4 no longer claims next week's ep 4) Co-Authored-By: Claude Opus 5.5 --- .../lib/src/schedule/schedule_model.dart | 17 +++++++++++++---- apps/desktop/test/schedule_model_test.dart | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/desktop/lib/src/schedule/schedule_model.dart b/apps/desktop/lib/src/schedule/schedule_model.dart index 219ef03..6b77bb6 100644 --- a/apps/desktop/lib/src/schedule/schedule_model.dart +++ b/apps/desktop/lib/src/schedule/schedule_model.dart @@ -140,17 +140,24 @@ List matchSchedule( final id = s.anilistId; if (id != null) refs[id] ??= _SeriesRef(s.seriesId, s.jellyfinSeriesId, s.followed); } + // AnimeFlow's AniList episode number wins outright: TVDB numbering runs + // on across split entries (JoJo S6E4 is "2nd & 3rd STAGE" ep 3), so a TVDB + // or absolute number, or an air day, is only a guess for AniList entries + // AnimeFlow gives no AniList numbers for. + final byAni = {}; final byEp = {}; final byAbs = {}; final byDay = {}; + final numbered = {}; for (final it in schedule) { final e = it.episode, id = e.anilistId; if (id == null) continue; final ref = refs[id] ??= _SeriesRef(it.seriesId, it.jellyfinSeriesId, it.followed); if (it.followed) ref.followed = true; - // AnimeFlow's AniList episode number first: TVDB numbering can run on - // across split cours. - if (e.anilistEpisode != null) byEp.putIfAbsent('$id#${e.anilistEpisode}', () => it); + if (e.anilistEpisode != null) { + numbered.add(id); + byAni.putIfAbsent('$id#${e.anilistEpisode}', () => it); + } byEp.putIfAbsent('$id#${e.episode}', () => it); if (e.absolute != null) byAbs.putIfAbsent('$id#${e.absolute}', () => it); if (e.airDate != null) byDay.putIfAbsent('$id@${_day(e.airDate!)}', () => it); @@ -160,7 +167,9 @@ List matchSchedule( () { final id = a.anilistId; final ref = refs[id]; - final it = byEp['$id#${a.episode}'] ?? byAbs['$id#${a.episode}'] ?? byDay['$id@${_day(a.airingAt)}']; + final it = numbered.contains(id) + ? byAni['$id#${a.episode}'] + : byEp['$id#${a.episode}'] ?? byAbs['$id#${a.episode}'] ?? byDay['$id@${_day(a.airingAt)}']; if (it != null) { final e = it.episode; return ScheduleEntry( diff --git a/apps/desktop/test/schedule_model_test.dart b/apps/desktop/test/schedule_model_test.dart index 8122afa..c7d0bdc 100644 --- a/apps/desktop/test/schedule_model_test.dart +++ b/apps/desktop/test/schedule_model_test.dart @@ -129,6 +129,21 @@ void main() { expect(out.single.season, 2); }); + test('a TVDB number never claims another AniList episode (JoJo S6E4 = 2nd STAGE ep 3)', () { + AnimeFlowScheduleItem row(int tvdbEp, int aniEp, EpisodeStatus st) => AnimeFlowScheduleItem( + episode: AnimeFlowEpisode(season: 6, episode: tvdbEp, anilistId: 210482, anilistEpisode: aniEp, status: st), + seriesId: 40, + title: 'JoJo', + jellyfinSeriesId: 'jfJ', + ); + final out = matchSchedule( + [airing(210482, 3, DateTime(2026, 10, 9, 11)), airing(210482, 4, DateTime(2026, 10, 16, 11))], + schedule: [row(4, 3, EpisodeStatus.available), row(5, 4, EpisodeStatus.upcoming)], + now: now, + ); + expect(out.map((e) => e.status), [ScheduleStatus.available, ScheduleStatus.upcoming]); + }); + test('by AniList id + episode, per season ids', () { // One AnimeFlow series (7) spans two AniList entries: S1 = 100, S2 = 200. final out = matchSchedule(