From a495a183d83d3d3d07e1951ff69cc52918d17b7f Mon Sep 17 00:00:00 2001 From: Dustin Date: Sat, 1 Aug 2026 17:44:05 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Login-Fehlerhandler=20=E2=80=94=20200=20?= =?UTF-8?q?ohne=20Token=20abfangen,=20Server-Nachrichten=20parsen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/database/db_helper.dart | 28 ++++++++++++++-- lib/models/song.dart | 4 +++ lib/services/auth_service.dart | 7 ++-- lib/services/download_service.dart | 30 +++++++++++++++++ lib/services/musik_scanner.dart | 54 ++++++++++++++++++++++++++++++ lib/widgets/song_tile.dart | 6 ++-- 6 files changed, 122 insertions(+), 7 deletions(-) diff --git a/lib/database/db_helper.dart b/lib/database/db_helper.dart index 2ec3634..2037592 100644 --- a/lib/database/db_helper.dart +++ b/lib/database/db_helper.dart @@ -20,7 +20,7 @@ class DbHelper { final pfad = await getDatabasesPath(); return openDatabase( p.join(pfad, 'melo.db'), - version: 3, + version: 4, onCreate: (db, version) async { await db.execute(''' CREATE TABLE songs ( @@ -37,6 +37,7 @@ class DbHelper { hinzugefuegt_am TEXT NOT NULL, download_quelle TEXT DEFAULT 'local', stream_url TEXT, + yt_url TEXT, zuletzt_position INTEGER ) '''); @@ -99,7 +100,13 @@ class DbHelper { // Spalte existiert bereits – ignorieren } } - // Weitere Migrationen: if (oldVersion < 4) { ... } + if (oldVersion < 4) { + try { + await db.execute('ALTER TABLE songs ADD COLUMN yt_url TEXT'); + } catch (_) { + // Spalte existiert bereits – ignorieren + } + } }, ); } @@ -205,6 +212,23 @@ class DbHelper { return rows.map((r) => Song.fromMap(r)).toList(); } + /// YouTube-Quell-URL für einen Song speichern + Future ytUrlAktualisieren(int songId, String ytUrl) async { + final d = await db; + await d.update('songs', {'yt_url': ytUrl}, + where: 'id = ?', whereArgs: [songId]); + } + + /// Alle Songs ohne YouTube-Quell-URL abrufen (für Scanner-Suche) + Future> songsOhneYtUrl({int limit = 50}) async { + final d = await db; + final rows = await d.query('songs', + where: 'yt_url IS NULL OR yt_url = ""', + orderBy: 'hinzugefuegt_am DESC', + limit: limit); + return rows.map((r) => Song.fromMap(r)).toList(); + } + // ─── Tags ──────────────────────────────────────── Future tagErstellen(String name, {String? icon, String? farbe}) async { diff --git a/lib/models/song.dart b/lib/models/song.dart index d4676bf..fe0eb84 100644 --- a/lib/models/song.dart +++ b/lib/models/song.dart @@ -12,6 +12,7 @@ class Song { final String hinzugefuegtAm; final String downloadQuelle; // "local", "youtube", "server" final String? streamUrl; // Für Server-Streaming (Navidrome) – wird bewusst NICHT in der DB persistiert (Auth-Token-Schutz) + final String? ytUrl; // YouTube-Quell-URL für Auto-Redownload bei korrupten Songs int? zuletztPosition; // Sekunden, für Wiederaufnahme Set? tagIds; // Cache für Tag-Filterung (nicht in DB gespeichert) @@ -29,6 +30,7 @@ class Song { String? hinzugefuegtAm, this.downloadQuelle = 'local', this.streamUrl, + this.ytUrl, this.zuletztPosition, }) : hinzugefuegtAm = hinzugefuegtAm ?? DateTime.now().toIso8601String(); @@ -46,6 +48,7 @@ class Song { 'hinzugefuegt_am': hinzugefuegtAm, 'download_quelle': downloadQuelle, 'stream_url': null, // Token-haltige Stream-URLs nie persistieren (Sicherheit) + 'yt_url': ytUrl, 'zuletzt_position': zuletztPosition, }; @@ -63,6 +66,7 @@ class Song { hinzugefuegtAm: m['hinzugefuegt_am'] as String?, downloadQuelle: m['download_quelle'] as String? ?? 'local', streamUrl: m['stream_url'] as String?, + ytUrl: m['yt_url'] as String?, zuletztPosition: m['zuletzt_position'] as int?, ); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index f46eb70..83a7f2d 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -76,13 +76,16 @@ class AuthService { MeloLogger().aktion('auth_login_ok', {'user': _user}); return AuthResult.ok; } + // 200 aber kein Token — Server-Fehler + MeloLogger().fehler('auth_login_no_token', response.body); + return AuthResult.fehlgeschlagen('Server-Antwort ohne Token'); } - // Fehler vom Server parsen + // Nicht-200: Fehler vom Server parsen String fehler = 'Unbekannter Fehler'; try { final data = jsonDecode(response.body); - fehler = data['error'] as String? ?? 'Login fehlgeschlagen (${response.statusCode})'; + fehler = data['error'] as String? ?? data['message'] as String? ?? 'Login fehlgeschlagen (${response.statusCode})'; } catch (_) { fehler = 'Server nicht erreichbar (${response.statusCode})'; } diff --git a/lib/services/download_service.dart b/lib/services/download_service.dart index 961fc68..c13966d 100644 --- a/lib/services/download_service.dart +++ b/lib/services/download_service.dart @@ -318,6 +318,7 @@ class DownloadService extends ChangeNotifier { istHeruntergeladen: true, istKorrupt: istKorrupt, downloadQuelle: 'youtube', + ytUrl: url, ); await _db.songEinfuegen(song); @@ -426,6 +427,35 @@ class DownloadService extends ChangeNotifier { } } + /// Korrupten Song mit bekannter ytUrl neu herunterladen + Future reDownload(Song song) async { + if (song.ytUrl == null || song.ytUrl!.isEmpty) { + _fehler = 'Keine YouTube-Quell-URL für diesen Song'; + notifyListeners(); + return null; + } + + _resetStatus(); + _aktuellerTitel = '🔄 Lade neu: ${song.titel}'; + notifyListeners(); + + // Alten Song-Eintrag aus DB löschen + if (song.id != null) { + await _db.loeschSong(song.id!); + } + + // Alte Datei löschen + try { + final alteDatei = File(song.dateiPfad); + if (await alteDatei.exists()) { + await alteDatei.delete(); + } + } catch (_) {} + + // Neu herunterladen mit der gespeicherten ytUrl + return _downloadEinzeln(song.ytUrl!); + } + @override void dispose() { abbrechen(); diff --git a/lib/services/musik_scanner.dart b/lib/services/musik_scanner.dart index fee0056..d05b184 100644 --- a/lib/services/musik_scanner.dart +++ b/lib/services/musik_scanner.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; import '../models/song.dart'; import '../database/db_helper.dart'; import '../utils/audio_validator.dart'; @@ -107,6 +109,9 @@ class MusikScanner { _neueSongs = neue.length; } + // ── YouTube-Quell-URLs für Songs ohne ytUrl nachschlagen ── + await _sucheYtUrls(); + return gefunden; } @@ -198,4 +203,53 @@ class MusikScanner { final dot = name.lastIndexOf('.'); return dot > 0 ? name.substring(0, dot) : name; } + + /// Sucht YouTube-Quell-URLs für Songs ohne ytUrl (max 50 pro Scan, 1/s Rate-Limit) + Future _sucheYtUrls() async { + const maxSuchanfragen = 50; + const suchUrlBasis = 'https://yt.baka-net.de/api/search'; + + final songsOhneUrl = await _db.songsOhneYtUrl(limit: maxSuchanfragen); + if (songsOhneUrl.isEmpty) return; + + int gefunden = 0; + for (final song in songsOhneUrl) { + if (gefunden >= maxSuchanfragen) break; + + try { + final query = '${song.kuenstler} ${song.titel}'; + final url = '$suchUrlBasis?q=${Uri.encodeQueryComponent(query)}'; + + final antwort = await http.get(Uri.parse(url)).timeout( + const Duration(seconds: 10), + ); + + if (antwort.statusCode == 200) { + final daten = jsonDecode(antwort.body); + if (daten is List && daten.isNotEmpty) { + final erstes = daten[0]; + final ytUrl = erstes['url'] as String?; + if (ytUrl != null && ytUrl.isNotEmpty) { + await _db.ytUrlAktualisieren(song.id!, ytUrl); + gefunden++; + MeloLogger().zustand('yt_suche_treffer', { + 'song_id': song.id, + 'titel': song.titel, + 'yt_url': ytUrl, + }); + } + } + } + } catch (e) { + MeloLogger().fehler('yt_suche_fehler', '${song.titel}: $e'); + } + + // Rate-Limit: 1 Anfrage pro Sekunde + await Future.delayed(const Duration(seconds: 1)); + } + + if (gefunden > 0) { + MeloLogger().zustand('yt_suche_abschluss', {'gefunden': gefunden}); + } + } } diff --git a/lib/widgets/song_tile.dart b/lib/widgets/song_tile.dart index a9a5284..3fc611b 100644 --- a/lib/widgets/song_tile.dart +++ b/lib/widgets/song_tile.dart @@ -67,14 +67,14 @@ class SongTile extends StatelessWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - // ── Korrupt: Erneut herunterladen ── - if (song.istKorrupt && song.downloadQuelle != 'local' && onErneutHerunterladen != null) + // ── Korrupt mit ytUrl: 🔄 Neu laden ── + if (song.istKorrupt && song.ytUrl != null && onErneutHerunterladen != null) InkWell( borderRadius: BorderRadius.circular(50), onTap: () => onErneutHerunterladen!(song), child: const Padding( padding: EdgeInsets.all(6), - child: Icon(Icons.download_rounded, size: 16, color: MeloTheme.rot), + child: Text('🔄', style: TextStyle(fontSize: 16)), ), ), InkWell(