v2.32 — Defekte Musik erkennen (Magic Bytes, ffprobe, DB-Flag, UI)

This commit is contained in:
Dustin
2026-08-01 16:40:42 +02:00
parent 4a05c76199
commit 2d4c7c8b98
9 changed files with 352 additions and 4 deletions
+16
View File
@@ -168,4 +168,20 @@ class CloudService {
return false;
}
}
/// Ruft defekte/korrupte Songs vom Cloud-Server ab
Future<List<Map>> getCorrupted() async {
try {
final r = await http
.get(Uri.parse('$_base/api/cloud/corrupted'), headers: _authHeader)
.timeout(const Duration(seconds: 15));
if (r.statusCode == 200) {
final d = jsonDecode(r.body);
return List<Map>.from(d['corrupted'] ?? []);
}
} catch (e) {
MeloLogger().fehler('cloud_corrupted', e);
}
return [];
}
}
+52
View File
@@ -245,6 +245,7 @@ class DownloadService extends ChangeNotifier {
dateiPfad = '${dir.path}/$lokalerName';
final stopwatch2 = Stopwatch()..start();
bool istKorrupt = false;
try {
final mp3Antwort = await _retryHttpGet(
Uri.parse('$_proxyBasisUrl$mp3Url'),
@@ -266,6 +267,13 @@ class DownloadService extends ChangeNotifier {
final file = File(dateiPfad);
await file.writeAsBytes(mp3Antwort.bodyBytes);
// ── Magic-Byte-Prüfung: Echte MP3-Datei? ──
istKorrupt = !_hatValideMagicBytes(file);
if (istKorrupt) {
debugPrint('⚠️ Korrupte Datei erkannt (Magic Bytes): $dateiPfad');
MeloLogger().fehler('magic_bytes_check', 'Ungültiger MP3-Header in $dateiPfad');
}
MeloLogger().netzwerk('GET', '$_proxyBasisUrl$mp3Url',
statusCode: 200, dauerMs: stopwatch2.elapsedMilliseconds);
@@ -307,6 +315,7 @@ class DownloadService extends ChangeNotifier {
dateiPfad: dateiPfad,
groesseBytes: await File(dateiPfad).length(),
istHeruntergeladen: true,
istKorrupt: istKorrupt,
downloadQuelle: 'youtube',
);
await _db.songEinfuegen(song);
@@ -327,6 +336,49 @@ class DownloadService extends ChangeNotifier {
}
}
/// Prüft die Magic Bytes einer Audiodatei.
/// MP3: ID3-Tag (49 44 33) oder MPEG-Frame (FF FB, FF FA, FF F3, FF F2)
/// M4A/AAC: ftyp-Box (66 74 79 70)
/// FLAC: fLaC (66 4C 61 43)
/// WAV: RIFF (52 49 46 46)
/// OGG: OggS (4F 67 67 53)
static bool _hatValideMagicBytes(File file) {
try {
if (!file.existsSync()) return false;
final bytes = file.readAsBytesSync().take(16).toList();
if (bytes.length < 4) return false;
// ID3v2 Tag (MP3 mit Metadaten) — Bytes: 49 44 33
if (bytes[0] == 0x49 && bytes[1] == 0x44 && bytes[2] == 0x33) return true;
// MP3 ohne ID3: MPEG Audio Frame Sync (FF FB, FF FA, FF F3, FF F2)
if (bytes[0] == 0xFF && (bytes[1] & 0xFE) == 0xFA) return true; // MPEG v1
if (bytes[0] == 0xFF && bytes[1] == 0xF3) return true; // MPEG v2 / v2.5
if (bytes[0] == 0xFF && bytes[1] == 0xF2) return true; // MPEG v2 / v2.5
// M4A/AAC: ftyp-Box
if (bytes.length >= 8 &&
bytes[4] == 0x66 && bytes[5] == 0x74 &&
bytes[6] == 0x79 && bytes[7] == 0x70) return true;
// FLAC: fLaC
if (bytes[0] == 0x66 && bytes[1] == 0x4C &&
bytes[2] == 0x61 && bytes[3] == 0x43) return true;
// WAV: RIFF
if (bytes[0] == 0x52 && bytes[1] == 0x49 &&
bytes[2] == 0x46 && bytes[3] == 0x46) return true;
// OGG: OggS
if (bytes[0] == 0x4F && bytes[1] == 0x67 &&
bytes[2] == 0x67 && bytes[3] == 0x53) return true;
return false;
} catch (_) {
return false;
}
}
/// HTTP POST mit Retry (exponentieller Backoff)
Future<http.Response> _retryHttpPost(
Uri url, {
+38
View File
@@ -51,6 +51,12 @@ class MusikScanner {
final stat = await file.stat();
final tags = Id3Reader.lesen(pfad);
// ── Magic-Byte-Prüfung ──
final istKorrupt = !_hatValideMagicBytes(pfad);
if (istKorrupt) {
MeloLogger().fehler('magic_bytes_scan', 'Ungültiger Audio-Header: $pfad');
}
// Cover-Bytes als Bilddatei speichern
String? coverPfad;
if (tags['cover'] != null && tags['cover'] is List<int>) {
@@ -79,6 +85,7 @@ class MusikScanner {
coverPfad: coverPfad,
groesseBytes: stat.size,
istHeruntergeladen: true,
istKorrupt: istKorrupt,
downloadQuelle: 'local',
));
} catch (e) {
@@ -185,6 +192,37 @@ class MusikScanner {
}
}
static bool _hatValideMagicBytes(String pfad) {
try {
final file = File(pfad);
if (!file.existsSync()) return false;
final bytes = file.readAsBytesSync().take(16).toList();
if (bytes.length < 4) return false;
// ID3v2 Tag (MP3) — 49 44 33
if (bytes[0] == 0x49 && bytes[1] == 0x44 && bytes[2] == 0x33) return true;
// MPEG Audio Frame Sync
if (bytes[0] == 0xFF && (bytes[1] & 0xFE) == 0xFA) return true;
if (bytes[0] == 0xFF && (bytes[1] == 0xF3 || bytes[1] == 0xF2)) return true;
// M4A/AAC
if (bytes.length >= 8 && bytes[4] == 0x66 && bytes[5] == 0x74 &&
bytes[6] == 0x79 && bytes[7] == 0x70) return true;
// FLAC
if (bytes[0] == 0x66 && bytes[1] == 0x4C &&
bytes[2] == 0x61 && bytes[3] == 0x43) return true;
// WAV
if (bytes[0] == 0x52 && bytes[1] == 0x49 &&
bytes[2] == 0x46 && bytes[3] == 0x46) { return true; }
// OGG
if (bytes[0] == 0x4F && bytes[1] == 0x67 &&
bytes[2] == 0x67 && bytes[3] == 0x53) { return true; }
return false;
} catch (_) {
return false;
}
}
String _dateiNameOhneEndung(String pfad) {
final name = pfad.split('/').last;
final dot = name.lastIndexOf('.');
+39
View File
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:just_audio/just_audio.dart';
import '../models/song.dart';
import '../database/db_helper.dart';
import 'melo_logger.dart';
class PlayerService {
@@ -14,6 +15,7 @@ class PlayerService {
final List<Song> _warteschlange = [];
int _aktuellerIndex = -1;
StreamSubscription<PlayerState>? _autoNextSub;
StreamSubscription<PlayerState>? _fehlerSub;
AudioPlayer get _p {
if (_player == null) {
@@ -23,6 +25,21 @@ class PlayerService {
naechstes();
}
});
// ── Playback-Fehler abfangen → Song als korrupt markieren ──
_fehlerSub = _player!.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) return; // bereits behandelt
// PlayerState hat kein explizites "failure"-Feld, aber wir prüfen ob
// die Quelle nicht geladen werden konnte (idle nach Fehlversuch)
if (state.processingState == ProcessingState.idle &&
_aktuellerIndex >= 0 &&
_aktuellerIndex < _warteschlange.length) {
final song = _warteschlange[_aktuellerIndex];
// Nur markieren wenn kein Stream (Stream-Fehler sind oft temporär)
if (song.streamUrl == null || song.streamUrl!.isEmpty) {
_markiereAlsKorrupt(song);
}
}
});
}
return _player!;
}
@@ -60,6 +77,11 @@ class PlayerService {
_songWechsel.add(song);
} catch (e) {
debugPrint('Fehler beim Abspielen: $e');
MeloLogger().fehler('player_abspiel_fehler', e);
// Song als korrupt markieren wenn lokale Datei
if (song.streamUrl == null || song.streamUrl!.isEmpty) {
_markiereAlsKorrupt(song);
}
}
}
@@ -100,8 +122,25 @@ class PlayerService {
void dispose() {
_autoNextSub?.cancel();
_fehlerSub?.cancel();
_player?.dispose();
_player = null;
_songWechsel.close();
}
/// Markiert den aktuellen Song in der DB als korrupt
Future<void> _markiereAlsKorrupt(Song song) async {
if (song.id == null) return;
try {
final db = DbHelper();
final s = await db.songNachId(song.id!);
if (s != null && !s.istKorrupt) {
await db.alsKorruptMarkieren(song.id!);
debugPrint('⚠️ Song als korrupt markiert: ${song.titel}');
MeloLogger().fehler('korrupt_markiert', 'Playback-Fehler: ${song.titel} (${song.dateiPfad})');
}
} catch (e) {
debugPrint('Korrupt-Markierung fehlgeschlagen: $e');
}
}
}