fix: Login-Fehlerhandler — 200 ohne Token abfangen, Server-Nachrichten parsen
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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})';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user