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>
52 lines
1.6 KiB
Dart
52 lines
1.6 KiB
Dart
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));
|
|
}
|