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; 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 setMovieResolution({required int movieId, required String resolution}) async { await _post({ 'action': 'set_movie_resolution', 'movie_id': movieId, 'resolution': resolution, }); } Future deleteMovie(int movieId) async { await _post({'action': 'delete_movie', 'movie_id': movieId}); } 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 { final map = await _post({'action': 'upsert_show', 'tmdb': tmdbJson}); return (map['id'] as num).toInt(); } 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 upsertEpisodesBulk(int seasonId, List> episodes) async { final map = await _post({ 'action': 'upsert_episodes_bulk', 'season_id': seasonId, 'episodes': episodes, }); return (map['count'] as num?)?.toInt() ?? 0; } Future> upsertSeasonsBulk(int showId, List> seasons) async { final map = await _post({ 'action': 'upsert_seasons_bulk', 'show_id': showId, 'seasons': seasons, }); final m = {}; 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> getCapabilities() async { final map = await _post({'action': 'get_capabilities'}); final caps = map['capabilities']; return caps is Map ? caps : (caps as Map).cast(); } 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(); } Future>> listShows() async { final map = await _post({'action': 'list_shows'}); return (map['items'] as List).cast>(); } Future 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 setShowMeta({ required int showId, String? resolution, String? downloadPath, bool? cliffhanger, }) async { final payload = { '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>> getSeriesSummary() async { final map = await _post({'action': 'get_series_summary'}); return (map['items'] as List).cast>(); } Future>> getShowEpisodes(int showId) async { final map = await _post({'action': 'get_show_episodes', 'show_id': showId}); return (map['items'] as List).cast>(); } Future deleteShow(int showId) async { await _post({'action': 'delete_show', 'show_id': showId}); } }