Film / Serie Update
This commit is contained in:
@@ -161,8 +161,9 @@ class BackendApi {
|
|||||||
return (map['items'] as List).cast<Map<String, dynamic>>();
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> upsertShow(Map<String, dynamic> tmdbJson) async {
|
Future<int> upsertShow(Map<String, dynamic> tmdbJson) async {
|
||||||
await _post({'action': 'upsert_show', 'tmdb': tmdbJson});
|
final map = await _post({'action': 'upsert_show', 'tmdb': tmdbJson});
|
||||||
|
return (map['id'] as num).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> upsertSeason(int showId, Map<String, dynamic> seasonJson) async {
|
Future<int> upsertSeason(int showId, Map<String, dynamic> seasonJson) async {
|
||||||
@@ -187,6 +188,17 @@ class BackendApi {
|
|||||||
return v == null ? null : (v as num).toInt();
|
return v == null ? null : (v as num).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, dynamic>>> listShows() async {
|
||||||
|
final map = await _post({'action': 'list_shows'});
|
||||||
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int?> 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<void> setShowMeta({
|
Future<void> setShowMeta({
|
||||||
required int showId,
|
required int showId,
|
||||||
String? resolution,
|
String? resolution,
|
||||||
|
|||||||
@@ -90,6 +90,35 @@ class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(m.title),
|
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: _dirty
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../../../core/status.dart';
|
|||||||
import '../data/movie_repository.dart';
|
import '../data/movie_repository.dart';
|
||||||
import 'widgets/status_chip.dart';
|
import 'widgets/status_chip.dart';
|
||||||
import 'movie_detail_screen.dart';
|
import 'movie_detail_screen.dart';
|
||||||
|
import '../../shared/providers.dart';
|
||||||
|
|
||||||
class MovieListScreen extends ConsumerWidget {
|
class MovieListScreen extends ConsumerWidget {
|
||||||
const MovieListScreen({super.key});
|
const MovieListScreen({super.key});
|
||||||
@@ -23,6 +24,71 @@ class MovieListScreen extends ConsumerWidget {
|
|||||||
selected: filter,
|
selected: filter,
|
||||||
onChanged: (f) => ref.read(movieFilterProvider.notifier).state = f,
|
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),
|
const SizedBox(height: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: moviesAsync.when(
|
child: moviesAsync.when(
|
||||||
@@ -169,4 +235,3 @@ class MovieListScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||||||
String? _overview;
|
String? _overview;
|
||||||
List<Map<String, dynamic>> _cast = const [];
|
List<Map<String, dynamic>> _cast = const [];
|
||||||
List<Map<String, dynamic>> _crew = const [];
|
List<Map<String, dynamic>> _crew = const [];
|
||||||
|
int? _tmdbId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -76,10 +77,11 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||||||
if (_downloadCtrl.text != (_downloadPath ?? '')) {
|
if (_downloadCtrl.text != (_downloadPath ?? '')) {
|
||||||
_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) {
|
if (first.showJson != null && first.showJson!.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final m = jsonDecode(first.showJson!) as Map<String, dynamic>;
|
final m = jsonDecode(first.showJson!) as Map<String, dynamic>;
|
||||||
|
_tmdbId = (m['id'] as num?)?.toInt() ?? _tmdbId;
|
||||||
_overview = (m['overview'] as String?) ?? _overview;
|
_overview = (m['overview'] as String?) ?? _overview;
|
||||||
final credits = m['credits'] as Map<String, dynamic>?;
|
final credits = m['credits'] as Map<String, dynamic>?;
|
||||||
if (credits != null) {
|
if (credits != null) {
|
||||||
@@ -237,6 +239,76 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(widget.showName),
|
title: Text(widget.showName),
|
||||||
actions: [
|
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)
|
if (dirty)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../movies/presentation/widgets/status_chip.dart';
|
|||||||
import '../data/series_repository.dart';
|
import '../data/series_repository.dart';
|
||||||
import 'widgets/episode_status_strip.dart';
|
import 'widgets/episode_status_strip.dart';
|
||||||
import 'series_detail_screen.dart';
|
import 'series_detail_screen.dart';
|
||||||
|
import '../../shared/providers.dart';
|
||||||
|
|
||||||
class SeriesListScreen extends ConsumerWidget {
|
class SeriesListScreen extends ConsumerWidget {
|
||||||
const SeriesListScreen({super.key});
|
const SeriesListScreen({super.key});
|
||||||
@@ -27,6 +28,211 @@ class SeriesListScreen extends ConsumerWidget {
|
|||||||
onChanged: (f) =>
|
onChanged: (f) =>
|
||||||
ref.read(episodeFilterProvider.notifier).state = 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),
|
const SizedBox(height: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: groupedAsync.when(
|
child: groupedAsync.when(
|
||||||
|
|||||||
+68
-6
@@ -170,8 +170,24 @@ try {
|
|||||||
}
|
}
|
||||||
$id = $pdo->lastInsertId();
|
$id = $pdo->lastInsertId();
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
if ($tmdbId) { $q=$pdo->prepare('SELECT id FROM episodes WHERE tmdb_id=?'); $q->execute([$tmdbId]); $id=$q->fetchColumn(); }
|
if ($tmdbId) {
|
||||||
if (!$id) { $q=$pdo->prepare('SELECT id FROM episodes WHERE season_id=? AND episode_number=?'); $q->execute([$seasonId,$epNo]); $id=$q->fetchColumn(); }
|
try {
|
||||||
|
$q=$pdo->prepare('SELECT id FROM episodes WHERE tmdb_id=?');
|
||||||
|
$q->execute([$tmdbId]);
|
||||||
|
$id=$q->fetchColumn();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) {
|
||||||
|
$q=$pdo->prepare('SELECT id FROM episodes WHERE season_id=? AND episode_number=?');
|
||||||
|
$q->execute([$seasonId,$epNo]);
|
||||||
|
$id=$q->fetchColumn();
|
||||||
|
} else { throw $e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$id) {
|
||||||
|
$q=$pdo->prepare('SELECT id FROM episodes WHERE season_id=? AND episode_number=?');
|
||||||
|
$q->execute([$seasonId,$epNo]);
|
||||||
|
$id=$q->fetchColumn();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
resp(['ok'=>true,'id'=>(int)$id]);
|
resp(['ok'=>true,'id'=>(int)$id]);
|
||||||
}
|
}
|
||||||
@@ -359,10 +375,56 @@ try {
|
|||||||
case 'get_show_by_tmdb': {
|
case 'get_show_by_tmdb': {
|
||||||
$tmdbId = (int)($in['tmdb_id'] ?? 0);
|
$tmdbId = (int)($in['tmdb_id'] ?? 0);
|
||||||
if (!$tmdbId) fail('bad params');
|
if (!$tmdbId) fail('bad params');
|
||||||
$stmt = $pdo->prepare('SELECT id FROM shows WHERE tmdb_id = ?');
|
try {
|
||||||
$stmt->execute([$tmdbId]);
|
$stmt = $pdo->prepare('SELECT id FROM shows WHERE tmdb_id = ?');
|
||||||
$id = $stmt->fetchColumn();
|
$stmt->execute([$tmdbId]);
|
||||||
resp(['ok' => true, 'id' => $id ? (int)$id : null]);
|
$id = $stmt->fetchColumn();
|
||||||
|
resp(['ok' => true, 'id' => $id ? (int)$id : null]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Fallback if column tmdb_id does not exist (older schema): scan JSON
|
||||||
|
if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) {
|
||||||
|
$stmt = $pdo->query('SELECT id, json FROM shows');
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
$found = null;
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$j = json_decode($r['json'] ?? 'null', true);
|
||||||
|
if (is_array($j) && isset($j['id']) && (int)$j['id'] === $tmdbId) {
|
||||||
|
$found = (int)$r['id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp(['ok' => true, 'id' => $found]);
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get_tmdb_by_show_id': {
|
||||||
|
$showId = (int)($in['show_id'] ?? 0);
|
||||||
|
if (!$showId) fail('bad params');
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare('SELECT tmdb_id FROM shows WHERE id = ?');
|
||||||
|
$stmt->execute([$showId]);
|
||||||
|
$tm = $stmt->fetchColumn();
|
||||||
|
resp(['ok' => true, 'tmdb_id' => $tm !== false ? (int)$tm : null]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Fallback if column tmdb_id does not exist: load from JSON
|
||||||
|
if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) {
|
||||||
|
$stmt = $pdo->prepare('SELECT json FROM shows WHERE id = ?');
|
||||||
|
$stmt->execute([$showId]);
|
||||||
|
$json = $stmt->fetchColumn();
|
||||||
|
$m = json_decode($json ?: 'null', true);
|
||||||
|
$tid = is_array($m) && isset($m['id']) ? (int)$m['id'] : null;
|
||||||
|
resp(['ok' => true, 'tmdb_id' => $tid]);
|
||||||
|
} else { throw $e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list_shows': {
|
||||||
|
$stmt = $pdo->query('SELECT id, tmdb_id, name, json FROM shows');
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
resp(['ok' => true, 'items' => $rows]);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'set_show_meta': {
|
case 'set_show_meta': {
|
||||||
|
|||||||
Reference in New Issue
Block a user