fix: Login-Fehlerhandler — 200 ohne Token abfangen, Server-Nachrichten parsen

This commit is contained in:
Dustin
2026-08-01 17:44:05 +02:00
parent 7f17a5afd6
commit a495a183d8
6 changed files with 122 additions and 7 deletions
+26 -2
View File
@@ -20,7 +20,7 @@ class DbHelper {
final pfad = await getDatabasesPath(); final pfad = await getDatabasesPath();
return openDatabase( return openDatabase(
p.join(pfad, 'melo.db'), p.join(pfad, 'melo.db'),
version: 3, version: 4,
onCreate: (db, version) async { onCreate: (db, version) async {
await db.execute(''' await db.execute('''
CREATE TABLE songs ( CREATE TABLE songs (
@@ -37,6 +37,7 @@ class DbHelper {
hinzugefuegt_am TEXT NOT NULL, hinzugefuegt_am TEXT NOT NULL,
download_quelle TEXT DEFAULT 'local', download_quelle TEXT DEFAULT 'local',
stream_url TEXT, stream_url TEXT,
yt_url TEXT,
zuletzt_position INTEGER zuletzt_position INTEGER
) )
'''); ''');
@@ -99,7 +100,13 @@ class DbHelper {
// Spalte existiert bereits ignorieren // 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(); return rows.map((r) => Song.fromMap(r)).toList();
} }
/// YouTube-Quell-URL für einen Song speichern
Future<void> 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<List<Song>> 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 ──────────────────────────────────────── // ─── Tags ────────────────────────────────────────
Future<int> tagErstellen(String name, {String? icon, String? farbe}) async { Future<int> tagErstellen(String name, {String? icon, String? farbe}) async {
+4
View File
@@ -12,6 +12,7 @@ class Song {
final String hinzugefuegtAm; final String hinzugefuegtAm;
final String downloadQuelle; // "local", "youtube", "server" 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? 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 int? zuletztPosition; // Sekunden, für Wiederaufnahme
Set<int>? tagIds; // Cache für Tag-Filterung (nicht in DB gespeichert) Set<int>? tagIds; // Cache für Tag-Filterung (nicht in DB gespeichert)
@@ -29,6 +30,7 @@ class Song {
String? hinzugefuegtAm, String? hinzugefuegtAm,
this.downloadQuelle = 'local', this.downloadQuelle = 'local',
this.streamUrl, this.streamUrl,
this.ytUrl,
this.zuletztPosition, this.zuletztPosition,
}) : hinzugefuegtAm = hinzugefuegtAm ?? DateTime.now().toIso8601String(); }) : hinzugefuegtAm = hinzugefuegtAm ?? DateTime.now().toIso8601String();
@@ -46,6 +48,7 @@ class Song {
'hinzugefuegt_am': hinzugefuegtAm, 'hinzugefuegt_am': hinzugefuegtAm,
'download_quelle': downloadQuelle, 'download_quelle': downloadQuelle,
'stream_url': null, // Token-haltige Stream-URLs nie persistieren (Sicherheit) 'stream_url': null, // Token-haltige Stream-URLs nie persistieren (Sicherheit)
'yt_url': ytUrl,
'zuletzt_position': zuletztPosition, 'zuletzt_position': zuletztPosition,
}; };
@@ -63,6 +66,7 @@ class Song {
hinzugefuegtAm: m['hinzugefuegt_am'] as String?, hinzugefuegtAm: m['hinzugefuegt_am'] as String?,
downloadQuelle: m['download_quelle'] as String? ?? 'local', downloadQuelle: m['download_quelle'] as String? ?? 'local',
streamUrl: m['stream_url'] as String?, streamUrl: m['stream_url'] as String?,
ytUrl: m['yt_url'] as String?,
zuletztPosition: m['zuletzt_position'] as int?, zuletztPosition: m['zuletzt_position'] as int?,
); );
+5 -2
View File
@@ -76,13 +76,16 @@ class AuthService {
MeloLogger().aktion('auth_login_ok', {'user': _user}); MeloLogger().aktion('auth_login_ok', {'user': _user});
return AuthResult.ok; 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'; String fehler = 'Unbekannter Fehler';
try { try {
final data = jsonDecode(response.body); 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 (_) { } catch (_) {
fehler = 'Server nicht erreichbar (${response.statusCode})'; fehler = 'Server nicht erreichbar (${response.statusCode})';
} }
+30
View File
@@ -318,6 +318,7 @@ class DownloadService extends ChangeNotifier {
istHeruntergeladen: true, istHeruntergeladen: true,
istKorrupt: istKorrupt, istKorrupt: istKorrupt,
downloadQuelle: 'youtube', downloadQuelle: 'youtube',
ytUrl: url,
); );
await _db.songEinfuegen(song); await _db.songEinfuegen(song);
@@ -426,6 +427,35 @@ class DownloadService extends ChangeNotifier {
} }
} }
/// Korrupten Song mit bekannter ytUrl neu herunterladen
Future<Song?> 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 @override
void dispose() { void dispose() {
abbrechen(); abbrechen();
+54
View File
@@ -3,6 +3,8 @@ import 'dart:io';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/song.dart'; import '../models/song.dart';
import '../database/db_helper.dart'; import '../database/db_helper.dart';
import '../utils/audio_validator.dart'; import '../utils/audio_validator.dart';
@@ -107,6 +109,9 @@ class MusikScanner {
_neueSongs = neue.length; _neueSongs = neue.length;
} }
// ── YouTube-Quell-URLs für Songs ohne ytUrl nachschlagen ──
await _sucheYtUrls();
return gefunden; return gefunden;
} }
@@ -198,4 +203,53 @@ class MusikScanner {
final dot = name.lastIndexOf('.'); final dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(0, dot) : name; 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<void> _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});
}
}
} }
+3 -3
View File
@@ -67,14 +67,14 @@ class SongTile extends StatelessWidget {
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// ── Korrupt: Erneut herunterladen ── // ── Korrupt mit ytUrl: 🔄 Neu laden ──
if (song.istKorrupt && song.downloadQuelle != 'local' && onErneutHerunterladen != null) if (song.istKorrupt && song.ytUrl != null && onErneutHerunterladen != null)
InkWell( InkWell(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
onTap: () => onErneutHerunterladen!(song), onTap: () => onErneutHerunterladen!(song),
child: const Padding( child: const Padding(
padding: EdgeInsets.all(6), padding: EdgeInsets.all(6),
child: Icon(Icons.download_rounded, size: 16, color: MeloTheme.rot), child: Text('🔄', style: TextStyle(fontSize: 16)),
), ),
), ),
InkWell( InkWell(