fix: code-review #2 — dedup MagicBytes, auth für check_corrupted, UI-Flag, merged player streams, jq-JSON
This commit is contained in:
@@ -27,6 +27,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
String _letzterSync = 'Nie';
|
String _letzterSync = 'Nie';
|
||||||
List<Map> _korrupteSongs = [];
|
List<Map> _korrupteSongs = [];
|
||||||
bool _ladtKorrupt = false;
|
bool _ladtKorrupt = false;
|
||||||
|
bool _hatGeprueft = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -94,12 +95,13 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _ladeKorrupteSongs() async {
|
Future<void> _ladeKorrupteSongs() async {
|
||||||
setState(() => _ladtKorrupt = true);
|
setState(() { _ladtKorrupt = true; _hatGeprueft = false; });
|
||||||
try {
|
try {
|
||||||
final corrupted = await widget.cloud.getCorrupted();
|
final corrupted = await widget.cloud.getCorrupted();
|
||||||
if (mounted) setState(() => _korrupteSongs = corrupted);
|
if (mounted) setState(() { _korrupteSongs = corrupted; _hatGeprueft = true; });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
MeloLogger().fehler('cloud_corrupted_laden', e);
|
MeloLogger().fehler('cloud_corrupted_laden', e);
|
||||||
|
if (mounted) setState(() => _hatGeprueft = true);
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _ladtKorrupt = false);
|
if (mounted) setState(() => _ladtKorrupt = false);
|
||||||
}
|
}
|
||||||
@@ -598,7 +600,7 @@ class _CloudScreenState extends State<CloudScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
] else if (!_ladtKorrupt && _korrupteSongs.isEmpty)
|
] else if (!_ladtKorrupt && _hatGeprueft && _korrupteSongs.isEmpty)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(top: 8),
|
padding: EdgeInsets.only(top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import '../models/song.dart';
|
import '../models/song.dart';
|
||||||
import '../database/db_helper.dart';
|
import '../database/db_helper.dart';
|
||||||
|
import '../utils/audio_validator.dart';
|
||||||
import 'melo_logger.dart';
|
import 'melo_logger.dart';
|
||||||
import '../config/app_config.dart';
|
import '../config/app_config.dart';
|
||||||
|
|
||||||
@@ -268,7 +269,7 @@ class DownloadService extends ChangeNotifier {
|
|||||||
await file.writeAsBytes(mp3Antwort.bodyBytes);
|
await file.writeAsBytes(mp3Antwort.bodyBytes);
|
||||||
|
|
||||||
// ── Magic-Byte-Prüfung: Echte MP3-Datei? ──
|
// ── Magic-Byte-Prüfung: Echte MP3-Datei? ──
|
||||||
istKorrupt = !_hatValideMagicBytes(file);
|
istKorrupt = !hatValideMagicBytes(file);
|
||||||
if (istKorrupt) {
|
if (istKorrupt) {
|
||||||
debugPrint('⚠️ Korrupte Datei erkannt (Magic Bytes): $dateiPfad');
|
debugPrint('⚠️ Korrupte Datei erkannt (Magic Bytes): $dateiPfad');
|
||||||
MeloLogger().fehler('magic_bytes_check', 'Ungültiger MP3-Header in $dateiPfad');
|
MeloLogger().fehler('magic_bytes_check', 'Ungültiger MP3-Header in $dateiPfad');
|
||||||
@@ -336,49 +337,6 @@ 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)
|
/// HTTP POST mit Retry (exponentieller Backoff)
|
||||||
Future<http.Response> _retryHttpPost(
|
Future<http.Response> _retryHttpPost(
|
||||||
Uri url, {
|
Uri url, {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import '../models/song.dart';
|
import '../models/song.dart';
|
||||||
import '../database/db_helper.dart';
|
import '../database/db_helper.dart';
|
||||||
|
import '../utils/audio_validator.dart';
|
||||||
import 'id3_reader.dart';
|
import 'id3_reader.dart';
|
||||||
import 'melo_logger.dart';
|
import 'melo_logger.dart';
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ class MusikScanner {
|
|||||||
final tags = Id3Reader.lesen(pfad);
|
final tags = Id3Reader.lesen(pfad);
|
||||||
|
|
||||||
// ── Magic-Byte-Prüfung ──
|
// ── Magic-Byte-Prüfung ──
|
||||||
final istKorrupt = !_hatValideMagicBytes(pfad);
|
final istKorrupt = !hatValideMagicBytes(file);
|
||||||
if (istKorrupt) {
|
if (istKorrupt) {
|
||||||
MeloLogger().fehler('magic_bytes_scan', 'Ungültiger Audio-Header: $pfad');
|
MeloLogger().fehler('magic_bytes_scan', 'Ungültiger Audio-Header: $pfad');
|
||||||
}
|
}
|
||||||
@@ -192,37 +193,6 @@ 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) {
|
String _dateiNameOhneEndung(String pfad) {
|
||||||
final name = pfad.split('/').last;
|
final name = pfad.split('/').last;
|
||||||
final dot = name.lastIndexOf('.');
|
final dot = name.lastIndexOf('.');
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class PlayerService {
|
|||||||
final List<Song> _warteschlange = [];
|
final List<Song> _warteschlange = [];
|
||||||
int _aktuellerIndex = -1;
|
int _aktuellerIndex = -1;
|
||||||
StreamSubscription<PlayerState>? _autoNextSub;
|
StreamSubscription<PlayerState>? _autoNextSub;
|
||||||
StreamSubscription<PlayerState>? _fehlerSub;
|
|
||||||
|
|
||||||
AudioPlayer get _p {
|
AudioPlayer get _p {
|
||||||
if (_player == null) {
|
if (_player == null) {
|
||||||
@@ -23,14 +22,7 @@ class PlayerService {
|
|||||||
_autoNextSub = _player!.playerStateStream.listen((state) {
|
_autoNextSub = _player!.playerStateStream.listen((state) {
|
||||||
if (state.processingState == ProcessingState.completed) {
|
if (state.processingState == ProcessingState.completed) {
|
||||||
naechstes();
|
naechstes();
|
||||||
}
|
} else if (state.processingState == ProcessingState.idle &&
|
||||||
});
|
|
||||||
// ── 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 >= 0 &&
|
||||||
_aktuellerIndex < _warteschlange.length) {
|
_aktuellerIndex < _warteschlange.length) {
|
||||||
final song = _warteschlange[_aktuellerIndex];
|
final song = _warteschlange[_aktuellerIndex];
|
||||||
@@ -39,7 +31,6 @@ class PlayerService {
|
|||||||
_markiereAlsKorrupt(song);
|
_markiereAlsKorrupt(song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return _player!;
|
return _player!;
|
||||||
}
|
}
|
||||||
@@ -122,7 +113,6 @@ class PlayerService {
|
|||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_autoNextSub?.cancel();
|
_autoNextSub?.cancel();
|
||||||
_fehlerSub?.cancel();
|
|
||||||
_player?.dispose();
|
_player?.dispose();
|
||||||
_player = null;
|
_player = null;
|
||||||
_songWechsel.close();
|
_songWechsel.close();
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Gemeinsame Utility: Validiert Magic Bytes von Audiodateien.
|
||||||
|
/// Prüft MP3 (ID3v2 + MPEG-Frames), M4A/AAC, FLAC, WAV, OGG.
|
||||||
|
///
|
||||||
|
/// 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)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user