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> _post(Map body) async { Future sendJson(Map p) { final prev = _dio.options.contentType; _dio.options.contentType = Headers.jsonContentType; return _dio.post('', data: p).whenComplete(() { _dio.options.contentType = prev; }); } Future sendForm(Map 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? 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; } if (res == null || res.statusCode != 200) { // ignore: avoid_print final sc = res?.statusCode; final data = res?.data; print('Backend HTTP ${sc}. URL: ${_dio.options.baseUrl}, payload: $payload, data: ${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) { // 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>> 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>(); } Future 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 upsertMovie(Map tmdbJson) async { await _post({ 'action': 'upsert_movie', 'tmdb': tmdbJson, }); } Future>> 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>(); } Future upsertShow(Map tmdbJson) async { await _post({'action': 'upsert_show', 'tmdb': tmdbJson}); } Future upsertSeason(int showId, Map seasonJson) async { final map = await _post( {'action': 'upsert_season', 'show_id': showId, 'tmdb': seasonJson}); return (map['id'] as num).toInt(); } Future upsertEpisode( int seasonId, Map episodeJson) async { final map = await _post({ 'action': 'upsert_episode', 'season_id': seasonId, 'tmdb': episodeJson }); return (map['id'] as num).toInt(); } Future 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(); } }