Files
plezy/lib/services/trackers/simkl/simkl_client.dart
T

93 lines
3.0 KiB
Dart

import 'dart:async';
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';
import 'simkl_constants.dart';
import 'simkl_session.dart';
/// HTTP wrapper for the Simkl REST API.
///
/// Simkl tokens don't expire; a 401 is terminal (user revoked access at
/// simkl.com/settings/apps). [onSessionInvalidated] clears the local session
/// in that case.
class SimklClient {
final SimklSession session;
final http.Client _http;
final void Function() onSessionInvalidated;
SimklClient(this.session, {required this.onSessionInvalidated, http.Client? httpClient})
: _http = httpClient ?? platform.createPlatformClient();
void dispose() => _http.close();
/// Fetch current user info. Used to populate the display name.
Future<Map<String, dynamic>?> getUserSettings() async {
final res = await _request('GET', '/users/settings');
return res is Map ? res.cast<String, dynamic>() : null;
}
/// Mark one or more items as watched. Body shape:
/// ```
/// {"movies": [{"ids": {"simkl": 123}}], "shows": [...]}
/// ```
Future<void> addToHistory(Map<String, dynamic> body) => _request('POST', '/sync/history', body: body);
Future<dynamic> _request(String method, String path, {Map<String, dynamic>? body}) async {
final uri = Uri.parse('${SimklConstants.apiBase}$path');
final headers = SimklConstants.headers(accessToken: session.accessToken);
final encoded = body == null ? null : json.encode(body);
final sw = Stopwatch()..start();
final res = await switch (method) {
'GET' || 'POST' => sendAbortableHttpRequest(
_http,
method,
uri,
headers: headers,
body: encoded,
timeout: TrackerConstants.requestTimeout,
operation: 'Simkl $method ${uri.path}',
),
_ => throw ArgumentError('Unsupported HTTP method: $method'),
};
sw.stop();
appLogger.d('Simkl $method ${uri.path}${res.statusCode} (${sw.elapsedMilliseconds}ms)');
if (res.statusCode == 401) {
onSessionInvalidated();
throw SimklAuthException('Session invalidated (401)');
}
if (res.statusCode < 200 || res.statusCode >= 300) {
throw SimklApiException(statusCode: res.statusCode, body: res.body);
}
if (res.body.isEmpty) return null;
try {
return json.decode(res.body);
} catch (_) {
return null;
}
}
}
class SimklApiException implements Exception {
final int statusCode;
final String body;
const SimklApiException({required this.statusCode, required this.body});
@override
String toString() => 'SimklApiException(HTTP $statusCode): $body';
}
class SimklAuthException implements Exception {
final String message;
const SimklAuthException(this.message);
@override
String toString() => 'SimklAuthException: $message';
}