Schedule: On Jellyfin only when the episode itself is there; Aired · not on Jellyfin yet otherwise

For shows AnimeFlow doesn't track, the app now looks the episode up in the
linked Jellyfin (one /Shows/{id}/Episodes per series, 3 min memo; TVDB
number via anilist_map offsets). No linked account: a neutral Aired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-10 20:15:18 +03:00
1 parent cc45b86271
commit 884f208330
7 files changed
+197 -18

No files matched your search

@@ -80,6 +80,36 @@ void main() {
expect(out.single.season, 6);
});
test('confirmOnJellyfin: the episode (with the run offset) must be on Jellyfin', () {
final lib = AnimeFlowLibraryItem.fromJson({
'jellyfin_series_id': 'jfJ',
'name': 'JoJo',
'anilist_map': [
{'season': 6, 'anilist_id': 210482, 'first_episode': 13, 'offset': 12},
],
});
final matched = matchSchedule(
[
airing(210482, 1, DateTime(2026, 10, 1, 18)), // S6E13: there
airing(210482, 2, DateTime(2026, 10, 8, 11)), // S6E14: not yet
airing(172192, 3, DateTime(2026, 10, 6, 18)), // series not looked up
],
schedule: const [],
library: [
lib,
const AnimeFlowLibraryItem(jellyfinSeriesId: 'jfR', name: 'Returner', anilistIds: {2: 172192}),
],
now: now,
);
expect(matched.map((e) => e.jellyfinEpisode), [13, 14, 3]);
final out = confirmOnJellyfin(matched, {
'jfJ': {(6, 12), (6, 13), (1, 14)},
});
expect(out.map((e) => e.status),
[ScheduleStatus.inLibrary, ScheduleStatus.notOnJellyfin, ScheduleStatus.aired]);
expect(out[1].jellyfinSeriesId, 'jfJ');
});
test("AnimeFlow's anilist_episode wins over TVDB numbering (split cour)", () {
// S2E13 in TVDB is part 2 (AniList 300) episode 1.
final out = matchSchedule(
+46
View File
@@ -8,6 +8,7 @@ import 'package:watch_together/src/jellyfin/accounts.dart';
import 'package:watch_together/src/schedule/anilist.dart';
import 'package:watch_together/src/schedule/animeflow.dart';
import 'package:watch_together/src/schedule/schedule_model.dart';
import 'package:watch_together/src/schedule/ui/schedule_actions.dart';
import 'package:watch_together/src/schedule/ui/schedule_page.dart';
import 'package:watch_together/src/screens/home/picked_media.dart';
@@ -434,6 +435,51 @@ void main() {
await done(tester);
});
testWidgets('untracked on Jellyfin: episode lookup, aired-not-there has no Play', (tester) async {
final jf = FakeJf();
final accounts = JellyfinAccounts(
store: MemoryJellyfinStore({
'accounts': jsonEncode([
{
'serverId': 'srv',
'serverName': 'Home',
'urls': ['http://jf.test'],
'userId': 'uid',
'userName': 'me',
'token': 'tok',
},
]),
}),
httpFactory: () => jf,
deviceName: 'Test',
);
final eps = await tester.runAsync(() => jellyfinEpisodeNumbers(accounts, flow, 'jf7'));
expect(eps, {(1, 2), (1, 3)});
await tester.runAsync(() => jellyfinEpisodeNumbers(accounts, flow, 'jf7'));
expect(jf.seen.where((u) => u.path.endsWith('/Episodes')), hasLength(1), reason: 'memoised');
final none = JellyfinAccounts(store: MemoryJellyfinStore(), deviceName: 'Test');
expect(await tester.runAsync(() => jellyfinEpisodeNumbers(none, flow, 'jf7')), isNull);
await pumpPage(
tester,
snap: () => snapshot(
list: [
ScheduleEntry(
airing: airing(777, 'Clover', 2, day(0, 9)),
status: ScheduleStatus.notOnJellyfin,
season: 1,
jellyfinEpisode: 2,
jellyfinSeriesId: 'jfC',
),
],
),
);
expect(card(777, 2), findsOneWidget);
expect(find.text('aired · not on Jellyfin yet'), findsOneWidget);
expect(action(777, 2), findsNothing);
await done(tester);
});
testWidgets('solo: the button says Play and works', (tester) async {
await pumpPage(tester, solo: true);
expect(find.text('Put it up'), findsNothing);