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(