Schedule backend: AniList client, AnimeFlow client, schedule matching
lib/src/schedule/ for the 1.0 Schedule tab (UI and docs come with the schedule round): - AniListClient: airingSchedules in a window, 50 per page, adult/music dropped; memory 10 min + disk 1 h cache, one Retry-After wait on 429, stale cache on failure; real User-Agent (AniList 403s some defaults). - AnimeFlowClient: the full app API (me, schedule, series, follow, search, requests incl. approve/reject, users/scopes) with the user's Jellyfin token, only for a Jellyfin on AnimeFlow's parent domain; ETag/304, typed error codes, backoff on nginx's HTML 429, /me cached at most 60 s so scope changes show. - matchSchedule + ScheduleRepository: AniList airings with AnimeFlow status by (episode anilist_id, episode), then absolute number, then air day; degrades to AniList-only when AnimeFlow fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c76fee70dc
commit
caa0765890
7 files changed
+2072
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
/// One request the fake server saw.
|
||||
class SeenRequest {
|
||||
final String method;
|
||||
final Uri uri;
|
||||
final HttpHeaders headers;
|
||||
final String body;
|
||||
SeenRequest(this.method, this.uri, this.headers, this.body);
|
||||
}
|
||||
|
||||
typedef FakeRoute = Future<void> Function(SeenRequest req, HttpResponse res);
|
||||
|
||||
/// A localhost HTTP server for the Schedule clients: [handler] answers,
|
||||
/// [seen] records every request.
|
||||
class FakeServer {
|
||||
FakeServer._(this._server) : url = 'http://${_server.address.host}:${_server.port}';
|
||||
final HttpServer _server;
|
||||
/// Kept after [close], so clients can try an address nobody answers.
|
||||
final String url;
|
||||
final seen = <SeenRequest>[];
|
||||
late FakeRoute handler;
|
||||
|
||||
static Future<FakeServer> start(FakeRoute handler) async {
|
||||
final s = FakeServer._(await HttpServer.bind(InternetAddress.loopbackIPv4, 0))..handler = handler;
|
||||
s._server.listen((req) async {
|
||||
final body = await utf8.decodeStream(req);
|
||||
final r = SeenRequest(req.method, req.uri, req.headers, body);
|
||||
s.seen.add(r);
|
||||
try {
|
||||
await s.handler(r, req.response);
|
||||
} catch (e, st) {
|
||||
// ignore: avoid_print
|
||||
print('fake server handler failed: $e\n$st');
|
||||
req.response.statusCode = 599;
|
||||
}
|
||||
await req.response.close();
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
Future<void> close() => _server.close(force: true);
|
||||
}
|
||||
|
||||
void json(HttpResponse res, Object? body, {int status = 200, String? etag}) {
|
||||
res.statusCode = status;
|
||||
res.headers.contentType = ContentType.json;
|
||||
if (etag != null) res.headers.set(HttpHeaders.etagHeader, etag);
|
||||
res.write(jsonEncode(body));
|
||||
}
|
||||
Reference in new issue
Block a user