Schedule: AniList entries split within a season (anilist_map, per-episode AniList id) count as on Jellyfin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff51d52fbb
commit
9a4b04672b
5 files changed
+53
-5
No files matched your search
@@ -148,13 +148,17 @@ class AnimeFlowLibraryItem {
|
|||||||
final String name;
|
final String name;
|
||||||
/// Set when AnimeFlow tracks it.
|
/// Set when AnimeFlow tracks it.
|
||||||
final int? seriesId;
|
final int? seriesId;
|
||||||
/// AniList entry per TVDB season.
|
/// AniList entry per TVDB season (the first, when a season spans more).
|
||||||
final Map<int, int> anilistIds;
|
final Map<int, int> anilistIds;
|
||||||
|
/// Every AniList entry per season with its episode run (`anilist_map`):
|
||||||
|
/// a season AniList splits (one cour as two entries) has several rows.
|
||||||
|
final List<AniListRun> anilistMap;
|
||||||
const AnimeFlowLibraryItem({
|
const AnimeFlowLibraryItem({
|
||||||
required this.jellyfinSeriesId,
|
required this.jellyfinSeriesId,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.seriesId,
|
this.seriesId,
|
||||||
this.anilistIds = const {},
|
this.anilistIds = const {},
|
||||||
|
this.anilistMap = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
static AnimeFlowLibraryItem fromJson(Map j) => AnimeFlowLibraryItem(
|
static AnimeFlowLibraryItem fromJson(Map j) => AnimeFlowLibraryItem(
|
||||||
@@ -165,9 +169,30 @@ class AnimeFlowLibraryItem {
|
|||||||
for (final e in ((j['anilist_ids'] as Map?) ?? const {}).entries)
|
for (final e in ((j['anilist_ids'] as Map?) ?? const {}).entries)
|
||||||
if (int.tryParse('${e.key}') case final season? when _int(e.value) != null) season: _int(e.value)!,
|
if (int.tryParse('${e.key}') case final season? when _int(e.value) != null) season: _int(e.value)!,
|
||||||
},
|
},
|
||||||
|
anilistMap: [
|
||||||
|
for (final r in (j['anilist_map'] as List?) ?? const [])
|
||||||
|
if (r is Map && _int(r['season']) != null && _int(r['anilist_id']) != null)
|
||||||
|
AniListRun(
|
||||||
|
season: _int(r['season'])!,
|
||||||
|
anilistId: _int(r['anilist_id'])!,
|
||||||
|
firstEpisode: _int(r['first_episode']) ?? 1,
|
||||||
|
offset: _int(r['offset']) ?? 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One AniList entry's run within a TVDB season: TVDB episode e (from
|
||||||
|
/// [firstEpisode] to the next run of that season) is AniList episode
|
||||||
|
/// e - [offset].
|
||||||
|
class AniListRun {
|
||||||
|
final int season;
|
||||||
|
final int anilistId;
|
||||||
|
final int firstEpisode;
|
||||||
|
final int offset;
|
||||||
|
const AniListRun({required this.season, required this.anilistId, this.firstEpisode = 1, this.offset = 0});
|
||||||
|
}
|
||||||
|
|
||||||
class AnimeFlowEpisode {
|
class AnimeFlowEpisode {
|
||||||
/// 0 = specials.
|
/// 0 = specials.
|
||||||
final int season;
|
final int season;
|
||||||
@@ -206,7 +231,9 @@ class AnimeFlowEpisode {
|
|||||||
absolute: _int(j['absolute']),
|
absolute: _int(j['absolute']),
|
||||||
title: _str(j['episode_title']),
|
title: _str(j['episode_title']),
|
||||||
airDate: _time(j['air_date']),
|
airDate: _time(j['air_date']),
|
||||||
anilistId: _int(j['anilist_id']),
|
// The episode's own entry (a split season rolls into its sequel),
|
||||||
|
// else the series' one.
|
||||||
|
anilistId: _int(j['episode_anilist_id']) ?? _int(j['anilist_id']),
|
||||||
anilistEpisode: _int(j['anilist_episode']),
|
anilistEpisode: _int(j['anilist_episode']),
|
||||||
status: EpisodeStatus.parse(j['status']),
|
status: EpisodeStatus.parse(j['status']),
|
||||||
progress: (j['progress'] as num?)?.toDouble(),
|
progress: (j['progress'] as num?)?.toDouble(),
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ List<ScheduleEntry> matchSchedule(
|
|||||||
final lib = <int, (String, int)>{};
|
final lib = <int, (String, int)>{};
|
||||||
for (final item in library) {
|
for (final item in library) {
|
||||||
if (item.jellyfinSeriesId.isEmpty) continue;
|
if (item.jellyfinSeriesId.isEmpty) continue;
|
||||||
|
for (final r in item.anilistMap) {
|
||||||
|
lib[r.anilistId] ??= (item.jellyfinSeriesId, r.season);
|
||||||
|
}
|
||||||
for (final e in item.anilistIds.entries) {
|
for (final e in item.anilistIds.entries) {
|
||||||
lib[e.value] ??= (item.jellyfinSeriesId, e.key);
|
lib[e.value] ??= (item.jellyfinSeriesId, e.key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -748,14 +748,14 @@ packages:
|
|||||||
path: "../../packages/core"
|
path: "../../packages/core"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
sync_relay_server:
|
sync_relay_server:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "../../packages/server"
|
path: "../../packages/server"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -63,6 +63,23 @@ void main() {
|
|||||||
expect(out[2].onJellyfin, isFalse);
|
expect(out[2].onJellyfin, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('anilist_map: a season split into two AniList entries is on Jellyfin', () {
|
||||||
|
final item = AnimeFlowLibraryItem.fromJson({
|
||||||
|
'jellyfin_series_id': 'jfJ',
|
||||||
|
'name': 'JoJo',
|
||||||
|
'anilist_ids': {'6': 190327},
|
||||||
|
'anilist_map': [
|
||||||
|
{'season': 6, 'anilist_id': 190327, 'first_episode': 1, 'offset': 0},
|
||||||
|
{'season': 6, 'anilist_id': 210482, 'first_episode': 2, 'offset': 1},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
final out = matchSchedule([airing(210482, 3, DateTime(2026, 10, 6, 18))],
|
||||||
|
schedule: const [], library: [item], now: now);
|
||||||
|
expect(out.single.status, ScheduleStatus.inLibrary);
|
||||||
|
expect(out.single.jellyfinSeriesId, 'jfJ');
|
||||||
|
expect(out.single.season, 6);
|
||||||
|
});
|
||||||
|
|
||||||
test("AnimeFlow's anilist_episode wins over TVDB numbering (split cour)", () {
|
test("AnimeFlow's anilist_episode wins over TVDB numbering (split cour)", () {
|
||||||
// S2E13 in TVDB is part 2 (AniList 300) episode 1.
|
// S2E13 in TVDB is part 2 (AniList 300) episode 1.
|
||||||
final out = matchSchedule(
|
final out = matchSchedule(
|
||||||
|
|||||||
@@ -592,7 +592,8 @@ always scrolls the page). Space never presses a focused button
|
|||||||
from AniList's airing schedule (`anilist.dart`: pages cached 10 min in
|
from AniList's airing schedule (`anilist.dart`: pages cached 10 min in
|
||||||
memory and 1 h on disk, stale data shown on 429 or failure) matched
|
memory and 1 h on disk, stale data shown on 429 or failure) matched
|
||||||
against AnimeFlow (`animeflow.dart`, app API with the Jellyfin token;
|
against AnimeFlow (`animeflow.dart`, app API with the Jellyfin token;
|
||||||
`/library` marks shows on Jellyfin it doesn't track) into a status per
|
`/library` marks shows on Jellyfin it doesn't track, through every AniList
|
||||||
|
entry of a season (`anilist_map`: a season AniList splits has several)) into a status per
|
||||||
episode (`schedule_model.dart`); requests and the owner's inbox are in
|
episode (`schedule_model.dart`); requests and the owner's inbox are in
|
||||||
`requests_sheet.dart`. It refreshes every minute; mouse drag and
|
`requests_sheet.dart`. It refreshes every minute; mouse drag and
|
||||||
Shift+wheel scroll the board sideways, the plain wheel scrolls a column.
|
Shift+wheel scroll the board sideways, the plain wheel scrolls a column.
|
||||||
|
|||||||
Reference in new issue
Block a user