Files
plezy/lib/services/trackers/tracker_http_client.dart
T
edde746 a33c423985 refactor(models): stop parsing wire fields and enum members nothing consumes
Parse-only DTO surface deleted across trackers and catalog models: quality-preset storageKey/fromStorage round-trips (persistence uses EnumPref on .name), PlexHome's placeholder identity fields (id stays — deriveHomeSecret reads it), SeerrUser.email/avatar, SimklSearchResult.endpointType, SimklDetail.type, SimklAllItemsEntry.status/isShow, TraktCatalogEntry.rank, CatalogItem.relevance, and the never-produced CatalogRankScope.favorited/.seasonal members with their label arms. Tracker root: OAuthProxyStart.expiresIn, TrackerSession.scope/isExpired, copyWith slimmed to its one real parameter, TrackerHttpClient.service. TrackerAccountStore.service and the trackerAccountStore() alias are live (persisted-blob validation, callers) and stay.
2026-08-17 19:02:24 +02:00

122 lines
3.7 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../utils/abortable_http_request.dart';
import '../../utils/app_logger.dart';
import '../../utils/platform_http_client_stub.dart'
if (dart.library.io) '../../utils/platform_http_client_io.dart'
as platform;
import 'tracker_constants.dart';
/// Transport shared by the tracker clients: builds, times and logs a request,
/// then hands back the raw response.
///
/// Status handling stays with each client because the rules genuinely differ:
/// MAL and Simkl accept any 2xx, Trakt a per-call set (200/201/204, plus 409
/// for scrobble), AniList only 200 (GraphQL errors ride a 200 body); and a 401
/// means refresh-and-retry for Trakt/MAL but a terminal session for AniList
/// and Simkl.
class TrackerHttpClient {
static const Set<String> allMethods = {'GET', 'POST', 'PATCH', 'PUT', 'DELETE'};
final String logLabel;
final http.Client _http;
TrackerHttpClient({required this.logLabel, http.Client? httpClient})
: _http = httpClient ?? platform.createPlatformClient();
void dispose() => _http.close();
Future<http.Response> send(
String method,
Uri uri, {
Map<String, String>? headers,
Object? body,
Duration timeout = TrackerConstants.requestTimeout,
Set<String> allowedMethods = allMethods,
String? operation,
}) async {
if (!allowedMethods.contains(method)) {
throw ArgumentError('Unsupported HTTP method: $method');
}
final op = operation ?? '$logLabel $method ${uri.path}';
final sw = Stopwatch()..start();
final res = await sendAbortableHttpRequest(
_http,
method,
uri,
headers: headers,
body: body,
timeout: timeout,
operation: op,
);
sw.stop();
appLogger.d('$op -> ${res.statusCode} (${sw.elapsedMilliseconds}ms)');
return res;
}
Future<http.Response> sendJson(
String method,
Uri uri, {
Map<String, String>? headers,
Map<String, dynamic>? body,
Duration timeout = TrackerConstants.requestTimeout,
Set<String> allowedMethods = allMethods,
String? operation,
}) {
return send(
method,
uri,
// A JSON body must declare its content type; without this, package:http
// treats a String body as text/plain.
headers: body == null ? headers : _withJsonContentType(headers),
body: body == null ? null : json.encode(body),
timeout: timeout,
allowedMethods: allowedMethods,
operation: operation,
);
}
static Map<String, String> _withJsonContentType(Map<String, String>? headers) {
final merged = Map<String, String>.from(headers ?? const {});
final hasContentType = merged.keys.any((key) => key.toLowerCase() == 'content-type');
if (!hasContentType) merged['Content-Type'] = 'application/json';
return merged;
}
Future<http.Response> sendForm(
String method,
Uri uri, {
required Map<String, String> headers,
required Map<String, String> body,
Duration timeout = TrackerConstants.requestTimeout,
Set<String> allowedMethods = allMethods,
String? operation,
}) {
final formHeaders = Map<String, String>.from(headers)..['Content-Type'] = 'application/x-www-form-urlencoded';
final encoded = body.entries
.map((e) => '${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}')
.join('&');
return send(
method,
uri,
headers: formHeaders,
body: encoded,
timeout: timeout,
allowedMethods: allowedMethods,
operation: operation,
);
}
static dynamic decodeJson(String body) {
if (body.isEmpty) return null;
try {
return json.decode(body);
} catch (_) {
return null;
}
}
}