|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import 'package:dio/dio.dart';
|
|
|
import '../../core/api/backend_api.dart';
|
|
|
import '../../core/config.dart';
|
|
|
import '../../core/async_utils.dart';
|
|
|
import '../shared/providers.dart';
|
|
|
|
|
|
class ImportScreen extends ConsumerStatefulWidget {
|
|
|
const ImportScreen({super.key});
|
|
|
|
|
|
@override
|
|
|
ConsumerState<ImportScreen> createState() => _ImportScreenState();
|
|
|
}
|
|
|
|
|
|
class _ImportScreenState extends ConsumerState<ImportScreen> {
|
|
|
final _movieCtrl =
|
|
|
TextEditingController(text: '603, 27205'); // Matrix, Inception
|
|
|
final _showCtrl =
|
|
|
TextEditingController(text: '1396, 1399'); // Breaking Bad, GoT
|
|
|
String _log = '';
|
|
|
bool _busy = false;
|
|
|
bool _cancel = false;
|
|
|
|
|
|
void _append(String msg) => setState(() => _log += '$msg\n');
|
|
|
|
|
|
Future<void> _importMovies() async {
|
|
|
setState(() => _busy = true);
|
|
|
final tmdb = ref.read(tmdbApiProvider);
|
|
|
final backend = ref.read(backendApiProvider);
|
|
|
|
|
|
final ids = _movieCtrl.text
|
|
|
.split(RegExp(r'[,\s]+'))
|
|
|
.where((s) => s.isNotEmpty)
|
|
|
.map(int.parse);
|
|
|
for (final id in ids) {
|
|
|
try {
|
|
|
_append('Film $id: TMDB laden …');
|
|
|
final json = await tmdb.getMovie(id);
|
|
|
await backend.upsertMovie(json);
|
|
|
_append('Film $id: OK ✓');
|
|
|
} catch (e) {
|
|
|
_append('Film $id: Fehler → $e');
|
|
|
}
|
|
|
}
|
|
|
setState(() => _busy = false);
|
|
|
}
|
|
|
|
|
|
Future<void> _importShows() async {
|
|
|
setState(() { _busy = true; _cancel = false; });
|
|
|
final tmdb = ref.read(tmdbApiProvider);
|
|
|
final backend = ref.read(backendApiProvider);
|
|
|
|
|
|
final ids = _showCtrl.text
|
|
|
.split(RegExp(r'[,\s]+'))
|
|
|
.where((s) => s.isNotEmpty)
|
|
|
.map(int.parse);
|
|
|
|
|
|
for (final showId in ids) {
|
|
|
if (_cancel) break;
|
|
|
try {
|
|
|
_append('Serie $showId: TMDB laden …');
|
|
|
final showJson = await tmdb.getShow(showId);
|
|
|
// Debug-Ausgabe
|
|
|
|
|
|
await backend.upsertShow(showJson);
|
|
|
_append('Serie $showId: Show OK ✓');
|
|
|
|
|
|
final seasonNos = (showJson['seasons'] as List? ?? const [])
|
|
|
.where((s) => (s['season_number'] ?? 0) is int)
|
|
|
.map((s) => (s as Map<String, dynamic>)['season_number'] as int)
|
|
|
.where((n) => n >= 0)
|
|
|
.toList();
|
|
|
|
|
|
// Fetch all seasons (limited concurrency)
|
|
|
final seasonsJson = <int, Map<String, dynamic>>{};
|
|
|
await runChunked<int>(
|
|
|
seasonNos,
|
|
|
AppConfig.tmdbSeasonFetchConcurrency,
|
|
|
(sNo) async {
|
|
|
if (_cancel) return;
|
|
|
final sj = await tmdb.getSeason(showId, sNo);
|
|
|
seasonsJson[sNo] = sj;
|
|
|
},
|
|
|
isCancelled: () => _cancel,
|
|
|
);
|
|
|
|
|
|
final dbShowId = await _getDbShowIdByTmdb(backend, showId);
|
|
|
// Bulk upsert seasons and get IDs
|
|
|
Map<int,int> seasonIds = const {};
|
|
|
try {
|
|
|
seasonIds = await backend.upsertSeasonsBulk(dbShowId, seasonsJson.values.toList());
|
|
|
} catch (_) {
|
|
|
final m = <int,int>{};
|
|
|
for (final entry in seasonsJson.entries) { final id = await backend.upsertSeason(dbShowId, entry.value); m[entry.key] = id; }
|
|
|
seasonIds = m;
|
|
|
}
|
|
|
|
|
|
for (final entry in seasonsJson.entries) {
|
|
|
if (_cancel) break;
|
|
|
final sNo = entry.key; final seasonJson = entry.value; final dbSeasonId = seasonIds[sNo]; if (dbSeasonId == null) continue;
|
|
|
_append(' S$sNo: upserting episodes …');
|
|
|
final eps = (seasonJson['episodes'] as List? ?? const []).cast<Map<String, dynamic>>();
|
|
|
try { await backend.upsertEpisodesBulk(dbSeasonId, eps); }
|
|
|
catch (_) {
|
|
|
await runChunked<Map<String, dynamic>>(
|
|
|
eps,
|
|
|
AppConfig.dbEpisodeUpsertConcurrency,
|
|
|
(e) async { if (_cancel) return; await backend.upsertEpisode(dbSeasonId, e); },
|
|
|
isCancelled: () => _cancel,
|
|
|
);
|
|
|
}
|
|
|
_append(' S$sNo: ${eps.length} Episoden OK ✓');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// 👇 Hier kommt der erweiterte Catch hin!
|
|
|
if (e is DioException) {
|
|
|
}
|
|
|
_append('Serie $showId: Fehler → $e');
|
|
|
}
|
|
|
}
|
|
|
setState(() => _busy = false);
|
|
|
}
|
|
|
|
|
|
Future<int> _getDbShowIdByTmdb(BackendApi backend, int tmdbId) async {
|
|
|
final id = await backend.getShowDbIdByTmdbId(tmdbId);
|
|
|
if (id == null) {
|
|
|
throw Exception(
|
|
|
'Show mit tmdb_id=$tmdbId nicht gefunden – zuerst upsert_show aufrufen.');
|
|
|
}
|
|
|
return id;
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
final inputStyle = const TextStyle(fontSize: 13);
|
|
|
|
|
|
return Scaffold(
|
|
|
appBar: AppBar(title: const Text('Import (TMDB → DB)')),
|
|
|
body: Padding(
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
child: Column(
|
|
|
children: [
|
|
|
Row(
|
|
|
children: [
|
|
|
const Text('Filme TMDB-IDs: '),
|
|
|
const SizedBox(width: 8),
|
|
|
Expanded(
|
|
|
child:
|
|
|
TextField(controller: _movieCtrl, style: inputStyle)),
|
|
|
const SizedBox(width: 8),
|
|
|
FilledButton(
|
|
|
onPressed: _busy ? null : _importMovies,
|
|
|
child: const Text('Import Filme'),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
const SizedBox(height: 12),
|
|
|
Row(
|
|
|
children: [
|
|
|
const Text('Serien TMDB-IDs: '),
|
|
|
const SizedBox(width: 8),
|
|
|
Expanded(
|
|
|
child: TextField(controller: _showCtrl, style: inputStyle)),
|
|
|
const SizedBox(width: 8),
|
|
|
FilledButton(
|
|
|
onPressed: _busy ? null : _importShows,
|
|
|
child: const Text('Import Serien'),
|
|
|
),
|
|
|
const SizedBox(width: 8),
|
|
|
if (_busy)
|
|
|
TextButton(
|
|
|
onPressed: () => setState(() => _cancel = true),
|
|
|
child: const Text('Abbrechen'),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
const SizedBox(height: 12),
|
|
|
if (_busy) const LinearProgressIndicator(),
|
|
|
const SizedBox(height: 12),
|
|
|
Expanded(
|
|
|
child: Container(
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
decoration: BoxDecoration(
|
|
|
border: Border.all(color: Colors.black12),
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
),
|
|
|
alignment: Alignment.topLeft,
|
|
|
child: SingleChildScrollView(
|
|
|
child: SelectableText(_log,
|
|
|
style: const TextStyle(
|
|
|
fontFamily: 'monospace', fontSize: 12)),
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|