Film / Serie Update

This commit is contained in:
2025-10-30 20:48:22 +01:00
parent 6ec3ad363c
commit dd21a3ed71
6 changed files with 456 additions and 10 deletions
@@ -90,6 +90,35 @@ class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(m.title),
actions: [
IconButton(
tooltip: 'TMDB aktualisieren',
icon: const Icon(Icons.sync),
onPressed: () async {
try {
final tmdb = ref.read(tmdbApiProvider);
final json = await tmdb.getMovie(m.tmdbId);
await ref.read(backendApiProvider).upsertMovie(json);
// Update local TMDB data for immediate UI refresh
if (mounted) setState(() => _tmdb = json);
// Refresh movie list so the list reflects new metadata
// ignore: unused_result
ref.invalidate(moviesProvider);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TMDB Daten aktualisiert')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('TMDB Update fehlgeschlagen: $e')),
);
}
}
},
)
],
),
floatingActionButton: _dirty
? FloatingActionButton.extended(
@@ -5,6 +5,7 @@ import '../../../core/status.dart';
import '../data/movie_repository.dart';
import 'widgets/status_chip.dart';
import 'movie_detail_screen.dart';
import '../../shared/providers.dart';
class MovieListScreen extends ConsumerWidget {
const MovieListScreen({super.key});
@@ -23,6 +24,71 @@ class MovieListScreen extends ConsumerWidget {
selected: filter,
onChanged: (f) => ref.read(movieFilterProvider.notifier).state = f,
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
icon: const Icon(Icons.sync),
label: const Text('TMDB: alle Filme updaten'),
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
String current = '';
int idx = 0;
int total = 0;
await showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) {
return StatefulBuilder(builder: (ctx, setState) {
Future<void> run() async {
try {
final tmdb = ref.read(tmdbApiProvider);
final backend = ref.read(backendApiProvider);
final items = await ref.read(moviesProvider.future);
total = items.length;
int ok = 0;
for (final m in items) {
idx++;
current = m.title;
setState(() {});
try {
final json = await tmdb.getMovie(m.tmdbId);
await backend.upsertMovie(json);
ok++;
} catch (_) {}
}
// Refresh list after update
// ignore: unused_result
ref.invalidate(moviesProvider);
messenger.showSnackBar(SnackBar(content: Text('TMDB Update fertig: $ok/$total Filme')));
} finally {
if (ctx.mounted) Navigator.of(ctx).pop();
}
}
if (idx == 0 && total == 0) {
// ignore: discarded_futures
run();
}
return AlertDialog(
title: const Text('TMDB Update'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Aktualisiere: $idx/$total'),
const SizedBox(height: 8),
Text(current, maxLines: 2, overflow: TextOverflow.ellipsis),
const SizedBox(height: 12),
LinearProgressIndicator(value: total > 0 ? idx / total : null),
],
),
);
});
},
);
},
),
),
const SizedBox(height: 12),
Expanded(
child: moviesAsync.when(
@@ -169,4 +235,3 @@ class MovieListScreen extends ConsumerWidget {
);
}
}
@@ -40,6 +40,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
String? _overview;
List<Map<String, dynamic>> _cast = const [];
List<Map<String, dynamic>> _crew = const [];
int? _tmdbId;
@override
void initState() {
@@ -76,10 +77,11 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
if (_downloadCtrl.text != (_downloadPath ?? '')) {
_downloadCtrl.text = _downloadPath ?? '';
}
// parse overview/cast/crew from show_json
// parse overview/cast/crew (and tmdb id) from show_json
if (first.showJson != null && first.showJson!.isNotEmpty) {
try {
final m = jsonDecode(first.showJson!) as Map<String, dynamic>;
_tmdbId = (m['id'] as num?)?.toInt() ?? _tmdbId;
_overview = (m['overview'] as String?) ?? _overview;
final credits = m['credits'] as Map<String, dynamic>?;
if (credits != null) {
@@ -237,6 +239,76 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
elevation: 0,
title: Text(widget.showName),
actions: [
IconButton(
tooltip: 'TMDB aktualisieren',
icon: const Icon(Icons.sync),
onPressed: _tmdbId == null
? null
: () async {
try {
final tmdb = ref.read(tmdbApiProvider);
final backend = ref.read(backendApiProvider);
int? tid = _tmdbId;
if (tid == null && _showId != null) {
tid = await backend.getTmdbIdByShowId(_showId!);
}
if (tid == null) {
throw 'tmdb_id für diese Serie konnte nicht ermittelt werden';
}
final showJson = await tmdb.getShow(tid);
// Update header meta immediately (overview/cast/crew)
try {
setState(() {
_overview = (showJson['overview'] as String?) ?? _overview;
final credits = showJson['credits'] as Map<String, dynamic>?;
if (credits != null) {
final c = (credits['cast'] as List? ?? const [])
.cast()
.map((e) => Map<String, dynamic>.from(e as Map))
.toList();
final cr = (credits['crew'] as List? ?? const [])
.cast()
.map((e) => Map<String, dynamic>.from(e as Map))
.toList();
_cast = c.take(12).toList();
_crew = cr.take(12).toList();
}
});
} catch (_) {}
final dbShowId = await backend.upsertShow(showJson);
// seasons
final seasons = (showJson['seasons'] as List? ?? const [])
.where((s) => (s['season_number'] ?? -1) is num)
.map((s) => (s as Map<String, dynamic>)['season_number'] as int)
.toList();
for (final sNo in seasons) {
if (sNo < 0) continue;
final seasonJson = await tmdb.getSeason(tid, sNo);
final seasonId = await backend.upsertSeason(dbShowId, seasonJson);
final eps = (seasonJson['episodes'] as List? ?? const [])
.cast<Map<String, dynamic>>();
for (final e in eps) {
await backend.upsertEpisode(seasonId, e);
}
}
// Refresh list + reload local grouped data
// ignore: unused_result
ref.invalidate(seriesGroupedProvider);
await _load();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TMDB Daten aktualisiert')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('TMDB Update fehlgeschlagen: $e')),
);
}
}
},
),
if (dirty)
IconButton(
icon: const Icon(Icons.save),
@@ -7,6 +7,7 @@ import '../../movies/presentation/widgets/status_chip.dart';
import '../data/series_repository.dart';
import 'widgets/episode_status_strip.dart';
import 'series_detail_screen.dart';
import '../../shared/providers.dart';
class SeriesListScreen extends ConsumerWidget {
const SeriesListScreen({super.key});
@@ -27,6 +28,211 @@ class SeriesListScreen extends ConsumerWidget {
onChanged: (f) =>
ref.read(episodeFilterProvider.notifier).state = f,
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
icon: const Icon(Icons.sync),
label: const Text('TMDB: alle Serien updaten'),
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
String current = '';
int idx = 0;
int total = 0;
await showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) {
// Stateful progress dialog
return StatefulBuilder(builder: (ctx, setState) {
Future<void> run() async {
try {
final tmdb = ref.read(tmdbApiProvider);
final backend = ref.read(backendApiProvider);
List<Map<String, dynamic>> showRows = [];
try {
showRows = await backend.listShows();
} catch (_) {
// Fallback: aus der aktuellen grouped-Liste tmdbId parsen
final grouped = await ref.read(seriesGroupedProvider.future);
for (final entry in grouped.data.entries) {
final flat = entry.value.values.expand((e) => e);
if (flat.isEmpty) continue;
final ep = flat.first;
if (ep.showJson == null) continue;
try {
final m = jsonDecode(ep.showJson!);
final t = (m['id'] as num?)?.toInt();
if (t != null) {
showRows.add({'tmdb_id': t, 'name': entry.key});
}
} catch (_) {}
}
}
total = showRows.length;
int ok = 0;
for (final row in showRows) {
idx++;
final tmdbId = (row['tmdb_id'] as num?)?.toInt();
current = (row['name'] as String?) ?? 'tmdb:$tmdbId';
setState(() {});
if (tmdbId == null) continue;
try {
final showJson = await tmdb.getShow(tmdbId);
final dbShowId = await backend.upsertShow(showJson);
final seasons = (showJson['seasons'] as List? ?? const [])
.where((s) => (s['season_number'] ?? -1) is num)
.map((s) => (s as Map<String, dynamic>)['season_number'] as int)
.toList();
for (final sNo in seasons) {
if (sNo < 0) continue;
final seasonJson = await tmdb.getSeason(tmdbId, sNo);
final seasonId = await backend.upsertSeason(dbShowId, seasonJson);
final eps = (seasonJson['episodes'] as List? ?? const [])
.cast<Map<String, dynamic>>();
for (final e in eps) {
await backend.upsertEpisode(seasonId, e);
}
}
ok++;
} catch (_) {}
}
// ignore: unused_result
ref.invalidate(seriesGroupedProvider);
messenger.showSnackBar(
SnackBar(content: Text('TMDB Update fertig: $ok/$total Serien')),
);
} finally {
if (ctx.mounted) Navigator.of(ctx).pop();
}
}
// kick off once when dialog builds first time
if (idx == 0 && total == 0) {
// ignore: discarded_futures
run();
}
return AlertDialog(
title: const Text('TMDB Update'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Aktualisiere: $idx/$total'),
const SizedBox(height: 8),
Text(current, maxLines: 2, overflow: TextOverflow.ellipsis),
const SizedBox(height: 12),
LinearProgressIndicator(value: total > 0 ? idx / total : null),
],
),
);
});
},
);
},
),
),
const SizedBox(height: 6),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
icon: const Icon(Icons.build),
label: const Text('TMDB: Serie reparieren (IDs)'),
onPressed: () async {
final ctrl = TextEditingController();
final ids = await showDialog<List<int>>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('TMDB IDs (Komma/Leerzeichen getrennt)'),
content: TextField(
controller: ctrl,
decoration: const InputDecoration(hintText: 'z.B. 1396, 1399'),
autofocus: true,
),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Abbrechen')),
FilledButton(
onPressed: () {
final parts = ctrl.text.split(RegExp(r'[\s,;]+'));
final list = <int>[];
for (final p in parts) {
final v = int.tryParse(p.trim());
if (v != null) list.add(v);
}
Navigator.of(ctx).pop(list);
},
child: const Text('Start'),
),
],
),
);
if (ids == null || ids.isEmpty) return;
await showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) {
String current = '';
int idx = 0;
final tmdb = ref.read(tmdbApiProvider);
final backend = ref.read(backendApiProvider);
return StatefulBuilder(builder: (ctx, setState) {
Future<void> run() async {
try {
int ok = 0;
for (final tmdbId in ids) {
idx++;
current = 'tmdb:$tmdbId';
setState(() {});
try {
final showJson = await tmdb.getShow(tmdbId);
await backend.upsertShow(showJson);
final seasons = (showJson['seasons'] as List? ?? const [])
.where((s) => (s['season_number'] ?? -1) is num)
.map((s) => (s as Map<String, dynamic>)['season_number'] as int)
.toList();
final dbShowId = await backend.getShowDbIdByTmdbId(tmdbId);
if (dbShowId == null) continue;
for (final sNo in seasons) {
final seasonJson = await tmdb.getSeason(tmdbId, sNo);
final seasonId = await backend.upsertSeason(dbShowId, seasonJson);
final eps = (seasonJson['episodes'] as List? ?? const [])
.cast<Map<String, dynamic>>();
for (final e in eps) {
await backend.upsertEpisode(seasonId, e);
}
}
ok++;
} catch (_) {}
}
// ignore: unused_result
ref.invalidate(seriesGroupedProvider);
} finally {
if (ctx.mounted) Navigator.of(ctx).pop();
}
}
if (idx == 0) {
// ignore: discarded_futures
run();
}
return AlertDialog(
title: const Text('TMDB Reparatur'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Aktualisiere: $idx/${ids.length}'),
const SizedBox(height: 8),
Text(current, maxLines: 2, overflow: TextOverflow.ellipsis),
const SizedBox(height: 12),
LinearProgressIndicator(value: idx / ids.length),
],
),
);
});
},
);
},
),
),
const SizedBox(height: 12),
Expanded(
child: groupedAsync.when(