Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e621b392af | ||
|
|
884f208330 |
No files matched your search
@@ -17,9 +17,16 @@ enum ScheduleStatus {
|
|||||||
unmonitored,
|
unmonitored,
|
||||||
/// AnimeFlow does not track this series (or is unreachable).
|
/// AnimeFlow does not track this series (or is unreachable).
|
||||||
notTracked,
|
notTracked,
|
||||||
/// Not tracked by AnimeFlow, but the series is on Jellyfin (`/library`):
|
/// Not tracked by AnimeFlow, the series is on Jellyfin (`/library`) and
|
||||||
/// the episode may be there; Put it up finds out.
|
/// so is the episode ([confirmOnJellyfin]; [matchSchedule] alone sets
|
||||||
inLibrary;
|
/// it from the series).
|
||||||
|
inLibrary,
|
||||||
|
/// Not tracked, the series is on Jellyfin, the episode aired but is not
|
||||||
|
/// there yet.
|
||||||
|
notOnJellyfin,
|
||||||
|
/// Not tracked, the series is on Jellyfin, the episode aired; whether
|
||||||
|
/// it is there is unknown (no linked account, lookup failed).
|
||||||
|
aired;
|
||||||
|
|
||||||
static ScheduleStatus of(EpisodeStatus s) => switch (s) {
|
static ScheduleStatus of(EpisodeStatus s) => switch (s) {
|
||||||
EpisodeStatus.available => available,
|
EpisodeStatus.available => available,
|
||||||
@@ -44,8 +51,12 @@ class ScheduleEntry {
|
|||||||
final bool followed;
|
final bool followed;
|
||||||
/// AnimeFlow's series id (follow, requests).
|
/// AnimeFlow's series id (follow, requests).
|
||||||
final int? seriesId;
|
final int? seriesId;
|
||||||
/// The AnimeFlow season of the matched episode (requests).
|
/// The AnimeFlow season of the matched episode (requests); untracked
|
||||||
|
/// on Jellyfin: the TVDB season of the AniList entry.
|
||||||
final int? season;
|
final int? season;
|
||||||
|
/// Untracked on Jellyfin: the TVDB episode number in [season] (AniList
|
||||||
|
/// episode + the `anilist_map` run's offset).
|
||||||
|
final int? jellyfinEpisode;
|
||||||
/// Play target once available.
|
/// Play target once available.
|
||||||
final String? jellyfinSeriesId;
|
final String? jellyfinSeriesId;
|
||||||
/// The series is on Jellyfin (tracked or not).
|
/// The series is on Jellyfin (tracked or not).
|
||||||
@@ -60,9 +71,23 @@ class ScheduleEntry {
|
|||||||
this.followed = false,
|
this.followed = false,
|
||||||
this.seriesId,
|
this.seriesId,
|
||||||
this.season,
|
this.season,
|
||||||
|
this.jellyfinEpisode,
|
||||||
this.jellyfinSeriesId,
|
this.jellyfinSeriesId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ScheduleEntry withStatus(ScheduleStatus s) => ScheduleEntry(
|
||||||
|
airing: airing,
|
||||||
|
status: s,
|
||||||
|
progress: progress,
|
||||||
|
reason: reason,
|
||||||
|
tracked: tracked,
|
||||||
|
followed: followed,
|
||||||
|
seriesId: seriesId,
|
||||||
|
season: season,
|
||||||
|
jellyfinEpisode: jellyfinEpisode,
|
||||||
|
jellyfinSeriesId: jellyfinSeriesId,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ScheduleEntry(${airing.anilistId}#${airing.episode} $status)';
|
String toString() => 'ScheduleEntry(${airing.anilistId}#${airing.episode} $status)';
|
||||||
}
|
}
|
||||||
@@ -98,15 +123,16 @@ List<ScheduleEntry> matchSchedule(
|
|||||||
DateTime? now,
|
DateTime? now,
|
||||||
}) {
|
}) {
|
||||||
final at = now ?? DateTime.now();
|
final at = now ?? DateTime.now();
|
||||||
// AniList id -> (Jellyfin series, its TVDB season) from /library.
|
// AniList id -> (Jellyfin series, its TVDB season, TVDB - AniList
|
||||||
final lib = <int, (String, int)>{};
|
// episode offset) from /library.
|
||||||
|
final lib = <int, (String, int, 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) {
|
for (final r in item.anilistMap) {
|
||||||
lib[r.anilistId] ??= (item.jellyfinSeriesId, r.season);
|
lib[r.anilistId] ??= (item.jellyfinSeriesId, r.season, r.offset);
|
||||||
}
|
}
|
||||||
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, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final refs = <int, _SeriesRef>{};
|
final refs = <int, _SeriesRef>{};
|
||||||
@@ -156,6 +182,7 @@ List<ScheduleEntry> matchSchedule(
|
|||||||
airing: a,
|
airing: a,
|
||||||
status: a.airingAt.isAfter(at) ? ScheduleStatus.upcoming : ScheduleStatus.inLibrary,
|
status: a.airingAt.isAfter(at) ? ScheduleStatus.upcoming : ScheduleStatus.inLibrary,
|
||||||
season: onJf.$2,
|
season: onJf.$2,
|
||||||
|
jellyfinEpisode: a.episode + onJf.$3,
|
||||||
jellyfinSeriesId: onJf.$1,
|
jellyfinSeriesId: onJf.$1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -171,6 +198,27 @@ List<ScheduleEntry> matchSchedule(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks [matchSchedule]'s [ScheduleStatus.inLibrary] entries (series
|
||||||
|
/// on Jellyfin) against the episodes Jellyfin has: [episodes] maps a
|
||||||
|
/// Jellyfin series id to its (season, episode) numbers. Found stays
|
||||||
|
/// inLibrary, absent becomes [ScheduleStatus.notOnJellyfin], a series
|
||||||
|
/// missing from [episodes] (not looked up) [ScheduleStatus.aired].
|
||||||
|
List<ScheduleEntry> confirmOnJellyfin(List<ScheduleEntry> entries, Map<String, Set<(int, int)>> episodes) => [
|
||||||
|
for (final e in entries)
|
||||||
|
if (e.status != ScheduleStatus.inLibrary)
|
||||||
|
e
|
||||||
|
else
|
||||||
|
switch (episodes[e.jellyfinSeriesId]) {
|
||||||
|
null => e.withStatus(ScheduleStatus.aired),
|
||||||
|
final have when have.contains((e.season, e.jellyfinEpisode)) => e,
|
||||||
|
_ => e.withStatus(ScheduleStatus.notOnJellyfin),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The (season, episode) numbers Jellyfin has of a series; null when it
|
||||||
|
/// cannot tell (no linked account for that server).
|
||||||
|
typedef JellyfinEpisodeLookup = Future<Set<(int, int)>?> Function(String jellyfinSeriesId);
|
||||||
|
|
||||||
/// One loaded window. Errors are display text; [entries] is empty only
|
/// One loaded window. Errors are display text; [entries] is empty only
|
||||||
/// when AniList failed with nothing cached.
|
/// when AniList failed with nothing cached.
|
||||||
class ScheduleSnapshot {
|
class ScheduleSnapshot {
|
||||||
@@ -206,12 +254,15 @@ class ScheduleSnapshot {
|
|||||||
|
|
||||||
/// Loads AniList and AnimeFlow for a window and merges them.
|
/// Loads AniList and AnimeFlow for a window and merges them.
|
||||||
class ScheduleRepository {
|
class ScheduleRepository {
|
||||||
ScheduleRepository({required this.aniList, this.animeFlow, DateTime Function()? now})
|
ScheduleRepository({required this.aniList, this.animeFlow, this.jellyfinEpisodes, DateTime Function()? now})
|
||||||
: _now = now ?? DateTime.now;
|
: _now = now ?? DateTime.now;
|
||||||
|
|
||||||
final AniListClient aniList;
|
final AniListClient aniList;
|
||||||
/// Null = not configured (AniList only).
|
/// Null = not configured (AniList only).
|
||||||
final AnimeFlowClient? animeFlow;
|
final AnimeFlowClient? animeFlow;
|
||||||
|
/// Confirms "on Jellyfin" per episode ([confirmOnJellyfin]); null =
|
||||||
|
/// never confirmed (aired entries read [ScheduleStatus.aired]).
|
||||||
|
final JellyfinEpisodeLookup? jellyfinEpisodes;
|
||||||
final DateTime Function() _now;
|
final DateTime Function() _now;
|
||||||
|
|
||||||
/// Last week's Monday 00:00 local .. 4 weeks later.
|
/// Last week's Monday 00:00 local .. 4 weeks later.
|
||||||
@@ -236,7 +287,8 @@ class ScheduleRepository {
|
|||||||
final flow = await flowF;
|
final flow = await flowF;
|
||||||
final airings = [...ani.airings]..sort((a, b) => a.airingAt.compareTo(b.airingAt));
|
final airings = [...ani.airings]..sort((a, b) => a.airingAt.compareTo(b.airingAt));
|
||||||
final entries = flow.error == null
|
final entries = flow.error == null
|
||||||
? matchSchedule(airings, schedule: flow.schedule, series: flow.series, library: flow.library, now: now)
|
? await _confirm(
|
||||||
|
matchSchedule(airings, schedule: flow.schedule, series: flow.series, library: flow.library, now: now))
|
||||||
: [for (final a in airings) untrackedEntry(a, now)];
|
: [for (final a in airings) untrackedEntry(a, now)];
|
||||||
return ScheduleSnapshot(
|
return ScheduleSnapshot(
|
||||||
from: start,
|
from: start,
|
||||||
@@ -249,6 +301,26 @@ class ScheduleRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks up the episodes of every series an inLibrary entry is on, in
|
||||||
|
/// parallel; a failed or impossible lookup leaves that series out.
|
||||||
|
Future<List<ScheduleEntry>> _confirm(List<ScheduleEntry> entries) async {
|
||||||
|
final lookup = jellyfinEpisodes;
|
||||||
|
final ids = {
|
||||||
|
for (final e in entries)
|
||||||
|
if (e.status == ScheduleStatus.inLibrary && e.jellyfinSeriesId != null) e.jellyfinSeriesId!,
|
||||||
|
};
|
||||||
|
final found = <String, Set<(int, int)>>{};
|
||||||
|
if (lookup != null) {
|
||||||
|
await Future.wait([
|
||||||
|
for (final id in ids)
|
||||||
|
lookup(id).then((eps) {
|
||||||
|
if (eps != null) found[id] = eps;
|
||||||
|
}, onError: (_) {}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return confirmOnJellyfin(entries, found);
|
||||||
|
}
|
||||||
|
|
||||||
Future<({AnimeFlowMe? me, List<AnimeFlowScheduleItem> schedule, List<AnimeFlowSeries> series, List<AnimeFlowLibraryItem> library, Object? error})>
|
Future<({AnimeFlowMe? me, List<AnimeFlowScheduleItem> schedule, List<AnimeFlowSeries> series, List<AnimeFlowLibraryItem> library, Object? error})>
|
||||||
_animeFlow(DateTime from, DateTime to, bool force) async {
|
_animeFlow(DateTime from, DateTime to, bool force) async {
|
||||||
final c = animeFlow;
|
final c = animeFlow;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ Future<({int season, int episode})> flowNumbering(AnimeFlowClient? client, Sched
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ep != null) return (season: ep.season, episode: ep.episode);
|
if (ep != null) return (season: ep.season, episode: ep.episode);
|
||||||
return (season: e.season ?? 1, episode: e.airing.episode);
|
return (season: e.season ?? 1, episode: e.jellyfinEpisode ?? e.airing.episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The AniDB id of an untracked AniList entry, from AnimeFlow's search:
|
/// The AniDB id of an untracked AniList entry, from AnimeFlow's search:
|
||||||
@@ -112,6 +112,25 @@ JellyfinAccount? jellyfinAccountFor(JellyfinAccounts accounts, AnimeFlowClient?
|
|||||||
return accounts.defaultAccount;
|
return accounts.defaultAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The (season, episode) numbers of the episodes Jellyfin has of
|
||||||
|
/// [jellyfinSeriesId], seen by the account AnimeFlow's ids belong to
|
||||||
|
/// ([jellyfinAccountFor]); null without a linked account. One request per
|
||||||
|
/// series, memoised a few minutes ([JellyfinAccounts.cached]).
|
||||||
|
Future<Set<(int, int)>?> jellyfinEpisodeNumbers(
|
||||||
|
JellyfinAccounts accounts,
|
||||||
|
AnimeFlowClient? flow,
|
||||||
|
String jellyfinSeriesId,
|
||||||
|
) async {
|
||||||
|
await accounts.ensureLoaded(validate: false);
|
||||||
|
final account = jellyfinAccountFor(accounts, flow);
|
||||||
|
final client = account == null ? null : accounts.clientFor(account.serverId);
|
||||||
|
if (account == null || client == null) return null;
|
||||||
|
return accounts.cached(account.serverId, 'schedule-episodes:$jellyfinSeriesId', () async => {
|
||||||
|
for (final x in await client.episodes(jellyfinSeriesId))
|
||||||
|
if (x.parentIndexNumber != null && x.indexNumber != null) (x.parentIndexNumber!, x.indexNumber!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// The Jellyfin episode of an available [e] as a media pick. Throws
|
/// The Jellyfin episode of an available [e] as a media pick. Throws
|
||||||
/// [ScheduleActionException] (or a client error) when it cannot be found.
|
/// [ScheduleActionException] (or a client error) when it cannot be found.
|
||||||
Future<JellyfinMedia> resolveJellyfinEpisode(JellyfinAccounts accounts, AnimeFlowClient? flow, ScheduleEntry e) async {
|
Future<JellyfinMedia> resolveJellyfinEpisode(JellyfinAccounts accounts, AnimeFlowClient? flow, ScheduleEntry e) async {
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ String countdownText(DateTime at, DateTime now) {
|
|||||||
ScheduleStatus.unmonitored => ('not monitored', Colors.grey, Icons.visibility_off_outlined),
|
ScheduleStatus.unmonitored => ('not monitored', Colors.grey, Icons.visibility_off_outlined),
|
||||||
ScheduleStatus.notTracked => ('not in AnimeFlow', Colors.grey, Icons.public),
|
ScheduleStatus.notTracked => ('not in AnimeFlow', Colors.grey, Icons.public),
|
||||||
ScheduleStatus.inLibrary => ('on Jellyfin', Colors.greenAccent, Icons.video_library_outlined),
|
ScheduleStatus.inLibrary => ('on Jellyfin', Colors.greenAccent, Icons.video_library_outlined),
|
||||||
|
ScheduleStatus.notOnJellyfin => ('aired · not on Jellyfin yet', Colors.grey, Icons.hourglass_empty),
|
||||||
|
ScheduleStatus.aired => ('aired', Colors.grey, Icons.done),
|
||||||
};
|
};
|
||||||
|
|
||||||
class DayColumn extends StatelessWidget {
|
class DayColumn extends StatelessWidget {
|
||||||
|
|||||||
@@ -97,18 +97,22 @@ class _SchedulePageState extends State<SchedulePage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_ownsRepo = widget.repository == null;
|
_ownsRepo = widget.repository == null;
|
||||||
_repo =
|
_repo = widget.repository ?? _ownRepo();
|
||||||
widget.repository ??
|
|
||||||
ScheduleRepository(
|
|
||||||
aniList: AniListClient(),
|
|
||||||
animeFlow: AnimeFlowClient.fromAccounts(JellyfinAccounts.instance),
|
|
||||||
);
|
|
||||||
_load();
|
_load();
|
||||||
_timer = Timer.periodic(SchedulePage.refreshEvery, (_) {
|
_timer = Timer.periodic(SchedulePage.refreshEvery, (_) {
|
||||||
if (mounted && !_refreshing && TickerMode.valuesOf(context).enabled) _load();
|
if (mounted && !_refreshing && TickerMode.valuesOf(context).enabled) _load();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScheduleRepository _ownRepo() {
|
||||||
|
final flow = AnimeFlowClient.fromAccounts(_accounts);
|
||||||
|
return ScheduleRepository(
|
||||||
|
aniList: AniListClient(),
|
||||||
|
animeFlow: flow,
|
||||||
|
jellyfinEpisodes: (id) => jellyfinEpisodeNumbers(_accounts, flow, id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
|||||||
+79
-50
@@ -5,18 +5,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: android_file_picker
|
name: android_file_picker
|
||||||
sha256: "014c74ab48d452c3252465682375a7fe6ddf56abb908ec361f207a3c4ffb2444"
|
sha256: "537bce9a43bc6fc08670cb68498251acc6eb9134b86a391b43e1f7c863588388"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "2.0.1"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
|
sha256: "3aa20c6b5617b632a1b7198dcca3d88065d37232485b9a6c6c14dc8f5357d279"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.4.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -61,10 +61,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: code_assets
|
name: code_assets
|
||||||
sha256: cfd4f5f575a49c5f10ca856e9846073f1e6c3ee94912377eea5f6cefc5272941
|
sha256: "828110d598123b5ea96c00c9f3c72105bf79f8ee36c20a39b26209ade421ec57"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.1.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -97,14 +97,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
cupertino_ui:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cupertino_ui
|
||||||
|
sha256: ea0b8170b40a86262b2fc21ac0625303f52df209dbf6fccc2b98b27eff44f6ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dbus
|
name: dbus
|
||||||
sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383
|
sha256: f7db8504e715b835f5ebe5fe74604ccd961d504c73389a5d8e81bafc2e2f3612
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.15"
|
version: "0.8.0"
|
||||||
dio:
|
dio:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -149,42 +157,42 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "9be6aac79508dbcf8dac80a4fb20f27ce6f910837ca27e3670b9295995cc0110"
|
sha256: "98c0b156b6380ba55bc767a14e2e6b3feb246e713ad40092424596fa79005bea"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.3.0"
|
version: "13.1.0"
|
||||||
file_picker_darwin:
|
file_picker_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_darwin
|
name: file_picker_darwin
|
||||||
sha256: "59fa5394cfa5b6dc8bf491630cb17077b77b844adbb840df371e7be5355d6cf6"
|
sha256: "3df1dc8e881de9ffd3953c9e482ac5d6e1ffe1e7f7b231b5695543e251ab7918"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "2.1.3"
|
||||||
file_picker_linux:
|
file_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_linux
|
name: file_picker_linux
|
||||||
sha256: bd52ff1e0048f29df95f913c55ad2991c72791d93e6c42241912c4bdee946cb9
|
sha256: c6562fc7d426db2fb85ea66fe2627953d2709932df5584ceaacf0aa5e39cf4da
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "2.0.2"
|
||||||
file_picker_platform_interface:
|
file_picker_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_platform_interface
|
name: file_picker_platform_interface
|
||||||
sha256: "0355558fd9af6da499d18e333d6b3beb44b0bf19e00933fccd15d18eb8d0e9ca"
|
sha256: bbdc085a6f168e63f147e9ce8988f79fa0800a58e7f25a48f762c60837107ba5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "4.0.0"
|
||||||
file_picker_web:
|
file_picker_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_web
|
name: file_picker_web
|
||||||
sha256: "935560a9d29fa6f006f2855addebb88e88d438e13627d4cc24187033c106f227"
|
sha256: "32af8262aca907b02b8970b32328b0312db0b42ba6f930be6d27fde179d59b7d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "4.1.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -210,10 +218,15 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -237,18 +250,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hive_ce
|
name: hive_ce
|
||||||
sha256: "8e9980e68643afb1e765d3af32b47996552a64e190d03faf622cea07c1294418"
|
sha256: "85d43209e2b4903a95d65d054407ea9ad4989d17b4d47e71732070d41b427c87"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.19.3"
|
version: "2.20.2"
|
||||||
hive_ce_flutter:
|
hive_ce_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hive_ce_flutter
|
name: hive_ce_flutter
|
||||||
sha256: "2677e95a333ff15af43ccd06af7eb7abbf1a4f154ea071997f3de4346cae913a"
|
sha256: "231c5f025a44f4a555b4e7417d1468027cc32d9471b00543bd5bce72464a1009"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
version: "2.4.0"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -277,10 +290,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"
|
sha256: "0be525eb917b032c7c9c980597c508a255814a858bd8d9abfc9c57e04f3ebf89"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.2"
|
version: "4.10.2"
|
||||||
|
intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.20.3"
|
||||||
isolate_channel:
|
isolate_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -293,18 +314,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: jni
|
name: jni
|
||||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
sha256: "6b9ca0602fef0230e83149afbe3f9f8227e94f2bbe84a1da4f14d699742f402d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.1.0"
|
||||||
jni_flutter:
|
jni_flutter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: jni_flutter
|
name: jni_flutter
|
||||||
sha256: b2310cdd4c18c65c081ab141a41efa94aa26c65431803703ece51996f174f351
|
sha256: "0dd33d67797560f66e961a6053882d4df54bd345bf063b8fe7ea133f7ad8b191"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.4+1"
|
||||||
jni_util:
|
jni_util:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -349,10 +370,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "6.1.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -377,6 +398,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.13.0"
|
||||||
|
material_ui:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_ui
|
||||||
|
sha256: "98ab9eda579ecae623e4b376c603035c5def78ce706f735f30bd632af5814695"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
media_kit:
|
media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -461,10 +490,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: objective_c
|
name: objective_c
|
||||||
sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3
|
sha256: "5c80c2ad6e2c0397de5db38c6d653e69616a3397e13aa7633bf39eb668f2041b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.6.0"
|
version: "9.6.2"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -477,10 +506,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481"
|
sha256: "73ef2cd5c11a7548f9b7f6fc2a4f6113de2e64aa2a70de63feede95548822908"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.2.1"
|
version: "10.2.2"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -549,10 +578,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
sha256: "2f1564aadb14215e478563b5406cb80729d022158c01dc50c96013c2f7eb4790"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.2"
|
version: "7.1.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -748,14 +777,14 @@ packages:
|
|||||||
path: "../../packages/core"
|
path: "../../packages/core"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.1.0"
|
version: "1.1.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.1.0"
|
version: "1.1.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -816,10 +845,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
|
sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.3"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -832,10 +861,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: wakelock_plus
|
name: wakelock_plus
|
||||||
sha256: "22b3e7e937721de70e63c85e7139f4ac781dc22863b9262431a53ac030eb074b"
|
sha256: "9a91139ee876c24a9ebe1484e4b9b7923b07d368566a5e0b2301870b0f5029c8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.1"
|
||||||
wakelock_plus_platform_interface:
|
wakelock_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -888,10 +917,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: windows_file_picker
|
name: windows_file_picker
|
||||||
sha256: "62e6e6e115231d1d1d71c7b5883b4091ce3e0190e49548dfb0ff8cba8d91ad96"
|
sha256: "18452aa293de3b6961c6d05b0e71b4be7bb90c67635abdd3aa5c4e75dd875072"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "2.0.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -904,10 +933,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
|
sha256: "0fe8cc98946def31cbcbc8734aa4067031e6f65ea3a78a508060bfed2200bfeb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.1.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -917,5 +946,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.13.0-0 <4.0.0"
|
dart: ">=3.13.0 <4.0.0"
|
||||||
flutter: ">=3.44.0"
|
flutter: ">=3.47.0"
|
||||||
@@ -18,7 +18,7 @@ dependencies:
|
|||||||
path: ../../packages/core
|
path: ../../packages/core
|
||||||
sync_relay_server:
|
sync_relay_server:
|
||||||
path: ../../packages/server
|
path: ../../packages/server
|
||||||
file_picker: ^12.3.0
|
file_picker: ^13.1.0
|
||||||
hive_ce: ^2.19.3
|
hive_ce: ^2.19.3
|
||||||
hive_ce_flutter: ^2.3.4
|
hive_ce_flutter: ^2.3.4
|
||||||
image: ^4.9.2
|
image: ^4.9.2
|
||||||
@@ -32,7 +32,7 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^6.0.0
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
|
|||||||
@@ -80,6 +80,36 @@ void main() {
|
|||||||
expect(out.single.season, 6);
|
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)", () {
|
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(
|
||||||
|
|||||||
@@ -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/anilist.dart';
|
||||||
import 'package:watch_together/src/schedule/animeflow.dart';
|
import 'package:watch_together/src/schedule/animeflow.dart';
|
||||||
import 'package:watch_together/src/schedule/schedule_model.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/schedule/ui/schedule_page.dart';
|
||||||
import 'package:watch_together/src/screens/home/picked_media.dart';
|
import 'package:watch_together/src/screens/home/picked_media.dart';
|
||||||
|
|
||||||
@@ -434,6 +435,51 @@ void main() {
|
|||||||
await done(tester);
|
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 {
|
testWidgets('solo: the button says Play and works', (tester) async {
|
||||||
await pumpPage(tester, solo: true);
|
await pumpPage(tester, solo: true);
|
||||||
expect(find.text('Put it up'), findsNothing);
|
expect(find.text('Put it up'), findsNothing);
|
||||||
|
|||||||
@@ -819,7 +819,13 @@ always scrolls the page). Space never presses a focused button
|
|||||||
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, through every AniList
|
`/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
|
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`). An aired episode of such a show reads
|
||||||
|
"on Jellyfin" only when Jellyfin has it: the season plus AniList
|
||||||
|
episode + the run's offset, checked against the series' episodes seen
|
||||||
|
by the linked account (`jellyfinEpisodeNumbers`, one request per series
|
||||||
|
memoised 3 min; `confirmOnJellyfin` is the pure step); absent reads
|
||||||
|
"aired · not on Jellyfin yet", unknown (no linked account, lookup
|
||||||
|
failed) "aired", neither with Put it up. 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.
|
||||||
Settings (`settings_tab.dart`) has a left nav with icons
|
Settings (`settings_tab.dart`) has a left nav with icons
|
||||||
|
|||||||
Reference in new issue
Block a user