You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
9.8 KiB
Dart
320 lines
9.8 KiB
Dart
import 'dart:convert';import 'package:dio/dio.dart';
|
|
import '../../core/config.dart';
|
|
|
|
class BackendApi {
|
|
final Dio _dio;
|
|
|
|
BackendApi()
|
|
: _dio = Dio(
|
|
BaseOptions(
|
|
baseUrl: AppConfig.backendBaseUrl,
|
|
headers: const {
|
|
'Accept': 'application/json',
|
|
},
|
|
contentType: Headers.jsonContentType,
|
|
validateStatus: (s) => s != null && s < 500,
|
|
),
|
|
);
|
|
|
|
Future<Map<String, dynamic>> _post(Map<String, dynamic> body) async {
|
|
Future<Response> sendJson(Map<String, dynamic> p) {
|
|
final prev = _dio.options.contentType;
|
|
_dio.options.contentType = Headers.jsonContentType;
|
|
return _dio.post('', data: p).whenComplete(() {
|
|
_dio.options.contentType = prev;
|
|
});
|
|
}
|
|
|
|
Future<Response> sendForm(Map<String, dynamic> p) {
|
|
final prev = _dio.options.contentType;
|
|
_dio.options.contentType = Headers.formUrlEncodedContentType;
|
|
final flat = p.map((k, v) => MapEntry(k, (v is Map || v is List) ? jsonEncode(v) : v));
|
|
return _dio.post('', data: flat).whenComplete(() {
|
|
_dio.options.contentType = prev;
|
|
});
|
|
}
|
|
|
|
Map<String, dynamic>? map;
|
|
Response? res;
|
|
final payload = {...body};
|
|
try {
|
|
res = await sendJson(payload);
|
|
} on DioException catch (e) {
|
|
// Capture 5xx with body
|
|
if (e.response != null) {
|
|
res = e.response;
|
|
} else {
|
|
// ignore: avoid_print
|
|
print('Backend request failed (JSON, no response). URL: ${_dio.options.baseUrl}, payload: $payload, error: $e');
|
|
throw Exception('Backend request failed: $e');
|
|
}
|
|
}
|
|
|
|
bool shouldFallbackToForm() {
|
|
final sc = res?.statusCode;
|
|
if (sc == 400) {
|
|
try {
|
|
final d = res?.data;
|
|
if (d is Map && (d['error']?.toString().toLowerCase().contains('unknown action') ?? false)) {
|
|
return true;
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (shouldFallbackToForm()) {
|
|
// ignore: avoid_print
|
|
print('Retrying request as form-urlencoded due to unknown action (server likely expects \$_POST).');
|
|
try {
|
|
res = await sendForm(payload);
|
|
} on DioException catch (e) {
|
|
if (e.response != null) {
|
|
res = e.response;
|
|
} else {
|
|
// ignore: avoid_print
|
|
print('Backend request failed (FORM, no response). URL: ${_dio.options.baseUrl}, payload: $payload, error: $e');
|
|
throw Exception('Backend request failed: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (res != null && res.data is Map) {
|
|
map = res.data as Map<String, dynamic>;
|
|
}
|
|
if (res == null || res.statusCode != 200) {
|
|
// ignore: avoid_print
|
|
final sc = res?.statusCode;
|
|
final data = res?.data;
|
|
if (map != null && map.containsKey('error')) {
|
|
throw Exception('Backend HTTP $sc: ${map['error']}');
|
|
}
|
|
throw Exception('Backend HTTP $sc: $data');
|
|
}
|
|
if (map == null) {
|
|
throw Exception('Backend: Unexpected response format');
|
|
}
|
|
if (map['ok'] != true && map['success'] != true) {
|
|
// ignore: avoid_print
|
|
print('Backend logical error. URL: ${_dio.options.baseUrl}, payload: $payload, data: $map');
|
|
throw Exception('Backend responded with error: ${map['error']}');
|
|
}
|
|
return map;
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getMovies(
|
|
{String? status, String? q, int offset = 0, int limit = 50}) async {
|
|
final map = await _post({
|
|
'action': 'get_list',
|
|
'type': 'movie',
|
|
if (status != null) 'status': status,
|
|
if (q != null && q.isNotEmpty) 'q': q,
|
|
'offset': offset,
|
|
'limit': limit,
|
|
});
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getGames(
|
|
{String? status, String? q, int offset = 0, int limit = 50, String? lang}) async {
|
|
final map = await _post({
|
|
'action': 'get_list',
|
|
'type': 'game',
|
|
if (status != null) 'status': status,
|
|
if (q != null && q.isNotEmpty) 'q': q,
|
|
if (lang != null && lang.isNotEmpty) 'lang': lang,
|
|
'offset': offset,
|
|
'limit': limit,
|
|
});
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
}
|
|
|
|
Future<void> setStatus({
|
|
required String type, // 'movie' | 'episode'
|
|
required int refId,
|
|
required String status, // 'Init' | 'Progress' | 'Done'
|
|
}) async {
|
|
await _post({
|
|
'action': 'set_status',
|
|
'type': type,
|
|
'ref_id': refId,
|
|
'status': status,
|
|
});
|
|
}
|
|
|
|
Future<void> upsertMovie(Map<String, dynamic> tmdbJson) async {
|
|
await _post({
|
|
'action': 'upsert_movie',
|
|
'tmdb': tmdbJson,
|
|
});
|
|
}
|
|
|
|
Future<void> setMovieResolution({required int movieId, required String resolution}) async {
|
|
await _post({
|
|
'action': 'set_movie_resolution',
|
|
'movie_id': movieId,
|
|
'resolution': resolution,
|
|
});
|
|
}
|
|
|
|
Future<void> deleteMovie(int movieId) async {
|
|
await _post({'action': 'delete_movie', 'movie_id': movieId});
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getEpisodes({
|
|
String? status,
|
|
String? q,
|
|
int offset = 0,
|
|
int limit = 5000,
|
|
}) async {
|
|
final map = await _post({
|
|
'action': 'get_list',
|
|
'type': 'episode',
|
|
if (status != null) 'status': status,
|
|
if (q != null && q.isNotEmpty) 'q': q,
|
|
'offset': offset,
|
|
'limit': limit,
|
|
});
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
}
|
|
|
|
Future<int> upsertShow(Map<String, dynamic> tmdbJson) async {
|
|
final map = await _post({'action': 'upsert_show', 'tmdb': tmdbJson});
|
|
return (map['id'] as num).toInt();
|
|
}
|
|
|
|
Future<int> upsertSeason(int showId, Map<String, dynamic> seasonJson) async {
|
|
final map = await _post(
|
|
{'action': 'upsert_season', 'show_id': showId, 'tmdb': seasonJson});
|
|
return (map['id'] as num).toInt();
|
|
}
|
|
|
|
Future<int> upsertEpisode(
|
|
int seasonId, Map<String, dynamic> episodeJson) async {
|
|
final map = await _post({
|
|
'action': 'upsert_episode',
|
|
'season_id': seasonId,
|
|
'tmdb': episodeJson
|
|
});
|
|
return (map['id'] as num).toInt();
|
|
}
|
|
|
|
Future<int> upsertEpisodesBulk(int seasonId, List<Map<String, dynamic>> episodes) async {
|
|
final map = await _post({
|
|
'action': 'upsert_episodes_bulk',
|
|
'season_id': seasonId,
|
|
'episodes': episodes,
|
|
});
|
|
return (map['count'] as num?)?.toInt() ?? 0;
|
|
}
|
|
|
|
Future<Map<int, int>> upsertSeasonsBulk(int showId, List<Map<String, dynamic>> seasons) async {
|
|
final map = await _post({
|
|
'action': 'upsert_seasons_bulk',
|
|
'show_id': showId,
|
|
'seasons': seasons,
|
|
});
|
|
final m = <int,int>{};
|
|
final any = map['map'];
|
|
if (any is Map) {
|
|
for (final e in any.entries) {
|
|
final k = int.tryParse(e.key.toString());
|
|
final v = (e.value as num?)?.toInt();
|
|
if (k != null && v != null) m[k] = v;
|
|
}
|
|
}
|
|
return m;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> getCapabilities() async {
|
|
final map = await _post({'action': 'get_capabilities'});
|
|
final caps = map['capabilities'];
|
|
return caps is Map<String, dynamic> ? caps : (caps as Map).cast<String, dynamic>();
|
|
}
|
|
|
|
Future<int?> getShowDbIdByTmdbId(int tmdbId) async {
|
|
final map = await _post({'action': 'get_show_by_tmdb', 'tmdb_id': tmdbId});
|
|
final v = map['id'];
|
|
return v == null ? null : (v as num).toInt();
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> listShows() async {
|
|
final map = await _post({'action': 'list_shows'});
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
}
|
|
|
|
Future<int?> getTmdbIdByShowId(int showId) async {
|
|
final map = await _post({'action': 'get_tmdb_by_show_id', 'show_id': showId});
|
|
final v = map['tmdb_id'];
|
|
return v == null ? null : (v as num).toInt();
|
|
}
|
|
|
|
Future<void> setShowMeta({
|
|
required int showId,
|
|
String? resolution,
|
|
String? downloadPath,
|
|
bool? cliffhanger,
|
|
}) async {
|
|
final payload = <String, dynamic>{
|
|
'action': 'set_show_meta',
|
|
'show_id': showId,
|
|
if (resolution != null) 'resolution': resolution,
|
|
if (downloadPath != null) 'download_path': downloadPath,
|
|
if (cliffhanger != null) 'cliffhanger': cliffhanger,
|
|
};
|
|
await _post(payload);
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getSeriesSummary() async {
|
|
final map = await _post({'action': 'get_series_summary'});
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getShowEpisodes(int showId) async {
|
|
final map = await _post({'action': 'get_show_episodes', 'show_id': showId});
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
}
|
|
|
|
Future<void> deleteShow(int showId) async {
|
|
await _post({'action': 'delete_show', 'show_id': showId});
|
|
}
|
|
|
|
Future<void> deleteGame(int igdbId) async {
|
|
await _post({'action': 'delete_game', 'igdb_id': igdbId});
|
|
}
|
|
|
|
Future<Map<String, dynamic>> setGameStatus({
|
|
required int igdbId,
|
|
required String name,
|
|
String? originalName,
|
|
required int status, // 0=Init,1=Progress,2=Done
|
|
String? note,
|
|
String? lang,
|
|
String? title,
|
|
String? summary,
|
|
String? storyline,
|
|
String? coverUrl,
|
|
int? releaseYear,
|
|
bool? locked,
|
|
Map<String, dynamic>? json,
|
|
}) async {
|
|
final map = await _post({
|
|
'action': 'set_game_status',
|
|
'igdb_id': igdbId,
|
|
'name': name,
|
|
if (originalName != null) 'original_name': originalName,
|
|
'status': status,
|
|
if (note != null) 'note': note,
|
|
if (lang != null && lang.isNotEmpty) 'lang': lang,
|
|
if (title != null) 'title': title,
|
|
if (summary != null) 'summary': summary,
|
|
if (storyline != null) 'storyline': storyline,
|
|
if (coverUrl != null) 'cover_url': coverUrl,
|
|
if (releaseYear != null) 'release_year': releaseYear,
|
|
if (locked != null) 'locked': locked,
|
|
if (json != null) 'json': json,
|
|
});
|
|
return (map['game'] as Map).cast<String, dynamic>();
|
|
}
|
|
}
|