diff --git a/README.md b/README.md index ba7a1fe..54207a9 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,8 @@ marked with what the Jellyfin server has of it when AnimeFlow is connected (Settings, Jellyfin & integrations): **on Jellyfin** (Put it up, right-click to queue), **downloading**, **waiting for release** (aired in Japan, AnimeFlow downloads it by itself once a release -appears), **failed**, **not monitored** (Request asks AnimeFlow for it), +appears; with when AnimeFlow last searched, or **release rejected** when +it found releases it won't take, the reason on hover), **failed**, **not monitored** (Request asks AnimeFlow for it), or the time left until it airs. A show AnimeFlow does not follow can be asked for with **Request series**. Episodes you already watched on Jellyfin carry a check on the cover; one you started shows how far you got. diff --git a/apps/desktop/lib/src/schedule/animeflow.dart b/apps/desktop/lib/src/schedule/animeflow.dart index 13dee92..fbd102d 100644 --- a/apps/desktop/lib/src/schedule/animeflow.dart +++ b/apps/desktop/lib/src/schedule/animeflow.dart @@ -210,8 +210,11 @@ class AnimeFlowEpisode { final EpisodeStatus status; /// 0..1, only while downloading. final double? progress; - /// Only when failed. + /// Why it failed; for a missing one with rejected releases, a + /// ready-to-show sentence ("2 release(s) found, rejected: …"). final String? reason; + /// Last search for a monitored, not yet downloaded episode. + final DateTime? lastSearchedAt; const AnimeFlowEpisode({ required this.season, @@ -224,6 +227,7 @@ class AnimeFlowEpisode { required this.status, this.progress, this.reason, + this.lastSearchedAt, }); static AnimeFlowEpisode fromJson(Map j) => AnimeFlowEpisode( @@ -239,6 +243,7 @@ class AnimeFlowEpisode { status: EpisodeStatus.parse(j['status']), progress: (j['progress'] as num?)?.toDouble(), reason: _str(j['reason']), + lastSearchedAt: _time(j['last_searched_at']), ); } diff --git a/apps/desktop/lib/src/schedule/schedule_model.dart b/apps/desktop/lib/src/schedule/schedule_model.dart index 31f4d88..731a5d7 100644 --- a/apps/desktop/lib/src/schedule/schedule_model.dart +++ b/apps/desktop/lib/src/schedule/schedule_model.dart @@ -48,8 +48,10 @@ class ScheduleEntry { final ScheduleStatus status; /// 0..1 while downloading. final double? progress; - /// Why it failed. + /// Why it failed, or (missing) why the releases found were rejected. final String? reason; + /// AnimeFlow's last search for it (missing). + final DateTime? lastSearchedAt; /// AnimeFlow tracks the series. final bool tracked; /// AnimeFlow's series id (requests). @@ -74,6 +76,7 @@ class ScheduleEntry { required this.status, this.progress, this.reason, + this.lastSearchedAt, this.tracked = false, this.seriesId, this.season, @@ -87,6 +90,7 @@ class ScheduleEntry { status: status ?? this.status, progress: progress, reason: reason, + lastSearchedAt: lastSearchedAt, tracked: tracked, seriesId: seriesId, season: season, @@ -231,7 +235,8 @@ List matchSchedule( airing: a, status: ScheduleStatus.of(e.status), progress: e.status == EpisodeStatus.downloading ? e.progress : null, - reason: e.status == EpisodeStatus.failed ? e.reason : null, + reason: e.status == EpisodeStatus.failed || e.status == EpisodeStatus.missing ? e.reason : null, + lastSearchedAt: e.lastSearchedAt, tracked: true, seriesId: it.seriesId, season: e.season, diff --git a/apps/desktop/lib/src/schedule/ui/schedule_card.dart b/apps/desktop/lib/src/schedule/ui/schedule_card.dart index ba7f31b..ffead1c 100644 --- a/apps/desktop/lib/src/schedule/ui/schedule_card.dart +++ b/apps/desktop/lib/src/schedule/ui/schedule_card.dart @@ -42,9 +42,21 @@ bool _waiting(ScheduleEntry e, DateTime now) => const waitingHint = 'Aired in Japan; downloads automatically when a release appears'; +String _ago(DateTime t, DateTime now) { + final m = now.difference(t).inMinutes; + return m < 1 ? 'just now' : m < 60 ? '$m min ago' : '${m ~/ 60} h ago'; +} + /// Status line: text, colour, icon. (String, Color, IconData) statusBadge(ScheduleEntry e, DateTime now) => _waiting(e, now) - ? ('waiting for release', Colors.amber.shade200, Icons.hourglass_top) + ? e.reason != null + // Releases exist but AnimeFlow rejected them (the hint says why). + ? ('release rejected', Colors.orangeAccent, Icons.block) + : ( + e.lastSearchedAt == null ? 'waiting for release' : 'waiting · searched ${_ago(e.lastSearchedAt!, now)}', + Colors.amber.shade200, + Icons.hourglass_top, + ) : switch (e.status) { ScheduleStatus.available => ('on Jellyfin', Colors.greenAccent, Icons.check_circle), ScheduleStatus.downloading => ( @@ -67,7 +79,11 @@ const waitingHint = 'Aired in Japan; downloads automatically when a release appe }; /// What the status line means, when its text alone does not say. -String? statusHint(ScheduleEntry e, DateTime now) => _waiting(e, now) ? waitingHint : null; +String? statusHint(ScheduleEntry e, DateTime now) => !_waiting(e, now) + ? null + : e.reason != null + ? '${e.reason}${e.lastSearchedAt == null ? '' : ' · searched ${_ago(e.lastSearchedAt!, now)}'}' + : waitingHint; class DayColumn extends StatelessWidget { final DateTime day; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b6abf45..d0d9653 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1188,7 +1188,10 @@ panel) and chat messages (Copy message, and a link in it). episode (`schedule_model.dart`). AnimeFlow's `missing` (aired, monitored, no release grabbed yet; AnimeFlow grabs it by itself once one appears), and its `upcoming` past the AniList air time, read - "waiting for release"; Request is offered only for an `unmonitored` + "waiting for release" (with "searched N min ago" from the episode's + `last_searched_at`, or "release rejected" with AnimeFlow's `reason` + sentence on hover when it found releases it won't take); Request is + offered only for an `unmonitored` episode (and "Request series" for a show AnimeFlow does not track). Air dates (AnimeFlow's `air_date`, midnight Japan time; Jellyfin's premiere date)