chore: init repository with project sources
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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;
|
||||
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<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<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<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<void> upsertShow(Map<String, dynamic> tmdbJson) async {
|
||||
await _post({'action': 'upsert_show', 'tmdb': tmdbJson});
|
||||
}
|
||||
|
||||
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?> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// lib/core/api/tmdb_api.dart
|
||||
import 'package:dio/dio.dart';
|
||||
import '../config.dart';
|
||||
|
||||
class TmdbApi {
|
||||
final Dio _dio;
|
||||
|
||||
TmdbApi()
|
||||
: _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: 'https://api.themoviedb.org/3',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
validateStatus: (code) => code != null && code < 500,
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> get _auth => {
|
||||
'api_key': AppConfig.tmdbApiKey,
|
||||
'language': 'de-DE',
|
||||
};
|
||||
|
||||
Future<Map<String, dynamic>> getMovie(int id) async {
|
||||
final res = await _dio.get(
|
||||
'/movie/$id',
|
||||
queryParameters: {
|
||||
..._auth,
|
||||
'append_to_response': 'images,credits',
|
||||
},
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception(
|
||||
'TMDB getMovie($id) failed: ${res.statusCode} ${res.data}');
|
||||
}
|
||||
return Map<String, dynamic>.from(res.data);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getShow(int id) async {
|
||||
final res = await _dio.get(
|
||||
'/tv/$id',
|
||||
queryParameters: {
|
||||
..._auth,
|
||||
'append_to_response': 'images,credits',
|
||||
},
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception(
|
||||
'TMDB getShow($id) failed: ${res.statusCode} ${res.data}');
|
||||
}
|
||||
return Map<String, dynamic>.from(res.data);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getSeason(int showId, int seasonNumber) async {
|
||||
final res = await _dio.get(
|
||||
'/tv/$showId/season/$seasonNumber',
|
||||
queryParameters: _auth,
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception(
|
||||
'TMDB getSeason($showId,S$seasonNumber) failed: ${res.statusCode} ${res.data}');
|
||||
}
|
||||
return Map<String, dynamic>.from(res.data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user