Schedule: watched marks and progress on episodes, "waiting for release" instead of a Request for missing ones, air dates matched as Japan days
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ec4fc8b13
commit
a407d3cbb0
9 files changed
+379
-117
No files matched your search
@@ -102,11 +102,43 @@ void main() {
|
||||
);
|
||||
expect(matched.map((e) => e.jellyfinEpisode), [13, 14, 3]);
|
||||
final out = confirmOnJellyfin(matched, {
|
||||
'jfJ': {(6, 12), (6, 13), (1, 14)},
|
||||
'jfJ': const [
|
||||
JellyfinEpisodeRef(6, 12),
|
||||
JellyfinEpisodeRef(6, 13, watch: EpisodeWatch(progress: 0.4)),
|
||||
JellyfinEpisodeRef(1, 14),
|
||||
],
|
||||
});
|
||||
expect(out.map((e) => e.status),
|
||||
[ScheduleStatus.inLibrary, ScheduleStatus.notOnJellyfin, ScheduleStatus.aired]);
|
||||
expect(out[1].jellyfinSeriesId, 'jfJ');
|
||||
expect(out.map((e) => e.watch), [const EpisodeWatch(progress: 0.4), null, null]);
|
||||
});
|
||||
|
||||
test("confirmOnJellyfin: AnimeFlow's available episodes get this user's watched state", () {
|
||||
final matched = matchSchedule(
|
||||
[
|
||||
airing(100, 3, DateTime(2026, 10, 1, 18)), // S1E3 by number
|
||||
airing(100, 4, DateTime.utc(2026, 10, 7, 14)), // numbering misses: premiere day
|
||||
airing(200, 1, DateTime(2026, 10, 1, 18)), // series not looked up
|
||||
],
|
||||
schedule: [
|
||||
item(100, 3, EpisodeStatus.available),
|
||||
item(100, 40, EpisodeStatus.available, absolute: 4),
|
||||
item(200, 1, EpisodeStatus.available),
|
||||
],
|
||||
now: now,
|
||||
);
|
||||
expect(matched.map((e) => e.jellyfinEpisode), [3, 40, 1]);
|
||||
final out = confirmOnJellyfin(matched, {
|
||||
'jf7': [
|
||||
const JellyfinEpisodeRef(1, 3, watch: EpisodeWatch(played: true)),
|
||||
// Midnight JST of Oct 7.
|
||||
JellyfinEpisodeRef(1, 4, premiere: DateTime.utc(2026, 10, 6, 15), watch: const EpisodeWatch(progress: 0.5)),
|
||||
],
|
||||
});
|
||||
expect(out.map((e) => e.status), everyElement(ScheduleStatus.available));
|
||||
expect(out.map((e) => e.watch),
|
||||
[const EpisodeWatch(played: true), const EpisodeWatch(progress: 0.5), null]);
|
||||
});
|
||||
|
||||
test("AnimeFlow's anilist_episode wins over TVDB numbering (split cour)", () {
|
||||
@@ -170,15 +202,22 @@ void main() {
|
||||
expect(out.single.tracked, isFalse);
|
||||
});
|
||||
|
||||
test('absolute number, then same local air day', () {
|
||||
test('absolute number, then the same Japan air day', () {
|
||||
final out = matchSchedule(
|
||||
[
|
||||
airing(300, 1100, DateTime(2026, 10, 5, 9)), // AniList counts absolute
|
||||
airing(400, 13, DateTime(2026, 10, 6, 23, 30)), // numbering differs
|
||||
// Numbering differs; Oct 6 23:30 JST, the evening before in
|
||||
// Europe and America.
|
||||
airing(400, 13, DateTime.utc(2026, 10, 6, 14, 30)),
|
||||
// Oct 8 01:30 JST, a late-night slot listed on Oct 7.
|
||||
airing(401, 13, DateTime.utc(2026, 10, 7, 16, 30)),
|
||||
],
|
||||
schedule: [
|
||||
item(300, 5, EpisodeStatus.available, season: 22, absolute: 1100),
|
||||
item(400, 1, EpisodeStatus.failed, season: 2, air: DateTime(2026, 10, 6, 0, 15), reason: 'No release'),
|
||||
// Air dates are midnight JST (Oct 6, 15:00 UTC the day before)...
|
||||
item(400, 1, EpisodeStatus.failed, season: 2, air: DateTime.utc(2026, 10, 5, 15), reason: 'No release'),
|
||||
// ... or midnight UTC from older servers.
|
||||
item(401, 1, EpisodeStatus.available, air: DateTime.utc(2026, 10, 7)),
|
||||
],
|
||||
now: now,
|
||||
);
|
||||
@@ -186,12 +225,14 @@ void main() {
|
||||
expect(out[0].season, 22);
|
||||
expect(out[1].status, ScheduleStatus.failed);
|
||||
expect(out[1].reason, 'No release');
|
||||
expect(out[2].status, ScheduleStatus.available);
|
||||
});
|
||||
|
||||
test('a different local day does not match', () {
|
||||
test('a different Japan day does not match', () {
|
||||
final out = matchSchedule(
|
||||
[airing(400, 13, DateTime(2026, 10, 7, 0, 30))],
|
||||
schedule: [item(400, 1, EpisodeStatus.available, air: DateTime(2026, 10, 6, 23, 50))],
|
||||
// Oct 7 20:00 JST; the air date is Oct 6 (midnight JST).
|
||||
[airing(400, 13, DateTime.utc(2026, 10, 7, 11))],
|
||||
schedule: [item(400, 1, EpisodeStatus.available, air: DateTime.utc(2026, 10, 5, 15))],
|
||||
now: now,
|
||||
);
|
||||
// Tracked (the id is in the schedule) but unmatched and aired.
|
||||
|
||||
@@ -50,6 +50,7 @@ class FakeJf implements JellyfinHttp {
|
||||
'SeriesName': 'Avail Show',
|
||||
'IndexNumber': 2,
|
||||
'ParentIndexNumber': 1,
|
||||
'UserData': {'Played': true},
|
||||
},
|
||||
{
|
||||
'Id': 'ep3',
|
||||
@@ -58,6 +59,8 @@ class FakeJf implements JellyfinHttp {
|
||||
'SeriesName': 'Avail Show',
|
||||
'IndexNumber': 3,
|
||||
'ParentIndexNumber': 1,
|
||||
'RunTimeTicks': 1000,
|
||||
'UserData': {'PlaybackPositionTicks': 250},
|
||||
},
|
||||
],
|
||||
'TotalRecordCount': 2,
|
||||
@@ -91,6 +94,7 @@ List<ScheduleEntry> entries() => [
|
||||
seriesId: 7,
|
||||
season: 1,
|
||||
jellyfinSeriesId: 'jf7',
|
||||
watch: const EpisodeWatch(played: true),
|
||||
),
|
||||
ScheduleEntry(
|
||||
airing: airing(222, 'Missing Show', 5, day(0, 10)),
|
||||
@@ -99,9 +103,8 @@ List<ScheduleEntry> entries() => [
|
||||
seriesId: 8,
|
||||
),
|
||||
ScheduleEntry(
|
||||
airing: airing(333, 'Failed Show', 2, day(0, 11)),
|
||||
status: ScheduleStatus.failed,
|
||||
reason: 'no seeders',
|
||||
airing: airing(333, 'Unmonitored Show', 2, day(0, 11)),
|
||||
status: ScheduleStatus.unmonitored,
|
||||
tracked: true,
|
||||
seriesId: 9,
|
||||
season: 1,
|
||||
@@ -283,8 +286,13 @@ void main() {
|
||||
expect(find.byKey(ValueKey('day-${w.year}-${w.month}-${w.day}')), findsNothing);
|
||||
expect(card(111, 3), findsOneWidget);
|
||||
expect(find.text('on Jellyfin'), findsOneWidget);
|
||||
expect(find.text('missing'), findsOneWidget);
|
||||
expect(find.text('failed · no seeders'), findsOneWidget);
|
||||
// AnimeFlow's "missing": waiting for a release, nothing to request;
|
||||
// only the unmonitored one has Request.
|
||||
expect(find.text('waiting for release'), findsOneWidget);
|
||||
expect(action(222, 5), findsNothing);
|
||||
expect(action(333, 2), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('watched-111-3')), findsOneWidget);
|
||||
expect(find.text('not monitored'), findsOneWidget);
|
||||
expect(find.text('downloading 42%'), findsOneWidget);
|
||||
expect(find.text('Put it up'), findsOneWidget);
|
||||
// On Jellyfin: untracked entries are hidden.
|
||||
@@ -316,7 +324,14 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('request on a tracked episode sends AnimeFlow numbering', (tester) async {
|
||||
await pumpPage(tester);
|
||||
await pumpPage(
|
||||
tester,
|
||||
snap: () => snapshot(
|
||||
list: [
|
||||
for (final e in entries()) e.airing.anilistId == 222 ? e.copyWith(status: ScheduleStatus.unmonitored) : e,
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.tap(action(222, 5));
|
||||
await io(tester);
|
||||
final post = server.seen.where((r) => r.method == 'POST' && r.uri.path.endsWith('/requests')).single;
|
||||
@@ -384,7 +399,7 @@ void main() {
|
||||
|
||||
testWidgets('without the request scope the button is disabled with a hint', (tester) async {
|
||||
await pumpPage(tester, snap: () => snapshot(me: viewer));
|
||||
final b = widgetOf<OutlinedButton>(tester, action(222, 5));
|
||||
final b = widgetOf<OutlinedButton>(tester, action(333, 2));
|
||||
expect(b.onPressed, isNull);
|
||||
expect(find.byWidgetPredicate((w) => w is Tooltip && (w.message ?? '').contains('owner')), findsWidgets);
|
||||
expect(find.text('My requests'), findsOneWidget);
|
||||
@@ -437,12 +452,13 @@ void main() {
|
||||
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'));
|
||||
final eps = await tester.runAsync(() => jellyfinScheduleEpisodes(accounts, flow, 'jf7'));
|
||||
expect(eps!.map((e) => (e.season, e.episode)), [(1, 2), (1, 3)]);
|
||||
expect(eps.map((e) => e.watch), [const EpisodeWatch(played: true), const EpisodeWatch(progress: 0.25)]);
|
||||
await tester.runAsync(() => jellyfinScheduleEpisodes(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);
|
||||
expect(await tester.runAsync(() => jellyfinScheduleEpisodes(none, flow, 'jf7')), isNull);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
|
||||
Reference in new issue
Block a user