# 🔮 Melo App — VollstĂ€ndiges Code-Review > **GeprĂŒft:** 20.07.2026 | **Projekt:** ~/Projects/melo_app/ | **Dustin (Baka) — Kiel** > **Prinzip:** Ponytail — minimaler Code, maximale Wirkung, max 500 Zeilen pro Datei --- ## 📋 Kritische Bugs (MÜSSEN SOFORT GEFIXT WERDEN) ### 🔮 [CRIT-1] Memory Leak: MiniPlayer-Streams werden nie gecancelled **Datei:** `widgets/mini_player.dart` · Zeilen 20–32 ```dart // initState abonniert Streams, aber speichert kein StreamSubscription _player.positionStream.listen((pos) { ... }); _player.stateStream.listen((state) { ... }); ``` **Problem:** `initState` abonniert `positionStream` und `stateStream` via `.listen()`, aber es gibt **kein** `dispose()`-Override. Die `StreamSubscription`-Objekte werden nirgends gespeichert, also können sie nie gecancelled werden. Jedes Mal wenn der MiniPlayer neu gebaut wird (z.B. bei setState im Parent), leakt ein neues Paar Subscriptions. **Fix:** ```dart StreamSubscription? _posSub; StreamSubscription? _stateSub; @override void initState() { super.initState(); _posSub = _player.positionStream.listen((pos) { ... }); _stateSub = _player.stateStream.listen((state) { ... }); } @override void dispose() { _posSub?.cancel(); _stateSub?.cancel(); super.dispose(); } ``` --- ### 🔮 [CRIT-2] Null-Crash: `widget.song.id!` kann explodieren **Datei:** `widgets/metadaten_dialog.dart` · Zeile 88 ```dart await _db.metadatenAktualisieren( widget.song.id!, // ← CRASH wenn id null ist! ... ); ``` **Problem:** `Song.id` ist `int?`. Wenn ein Song (z.B. frisch gescannter oder Beispielsong ohne DB-ID) den Metadaten-Editor öffnet, crasht die App mit `null check used on null value`. **Fix:** ```dart final id = widget.song.id; if (id == null) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Song-ID fehlt — bitte App neustarten')), ); return; } await _db.metadatenAktualisieren(id, ...); ``` --- ### 🔮 [CRIT-3] `dauerSekunden` ist IMMER 0 (kein Metadaten-Parsing) **Datei:** `services/musik_scanner.dart` · Zeilen 34–44 ```dart final song = Song( titel: _dateiNameOhneEndung(pfad), kuenstler: 'Unbekannt', // ← keinerlei Tag-Parsing dauerSekunden: 0, // ← IMMER 0! ... ); ``` **Problem:** Es wird nie die echte Audio-Dauer ausgelesen. Weder ID3-Tags noch MediaStore-Metadaten. Alle gescannten Songs haben `dauerSekunden: 0` → `dauerFormatiert` zeigt `"0:00"`. **Fix:** Nutze eine Audio-Metadaten-Bibliothek wie `flutter_media_metadata` oder `audio_metadata_reader`, oder implementiere einen nativen Method-Channel fĂŒr MediaStore-Query. Minimal-Fix: verwende `just_audio` kurz zum Öffnen der Datei um Duration zu lesen. --- ### 🔮 [CRIT-4] `positionAktualisieren` flutet DB mit immer neuen EintrĂ€gen **Datei:** `database/db_helper.dart` · Zeilen 126–135 ```dart Future positionAktualisieren(int songId, int position) async { final d = await db; await d.update('songs', {'zuletzt_position': position}, where: 'id = ?', whereArgs: [songId]); await d.insert('wiedergabe_verlauf', { // ← IMMER INSERT! 'song_id': songId, 'position': position, 'zuletzt_abgespielt': DateTime.now().toIso8601String(), }); } ``` **Problem:** Bei jedem Positions-Update wird ein NEUER History-Eintrag inserted, ohne den alten zu löschen. Nach 30 Minuten Musik hören mit 1-Sekunden-Takt → 1800 EintrĂ€ge pro Song. **Fix Variante A (einfach):** Lösche alten Eintrag vor neuem Insert: ```dart await d.delete('wiedergabe_verlauf', where: 'song_id = ?', whereArgs: [songId]); await d.insert('wiedergabe_verlauf', { ... }); ``` **Fix Variante B (besser):** Nur bei signifikanten Änderungen speichern (alle 30 Sekunden statt bei jedem Tick). --- ### 🔮 [CRIT-5] `FlutterErrorBoundary` tut GAR NICHTS **Datei:** `main.dart` · Zeilen 48–62 ```dart class _FlutterErrorBoundaryState extends State { @override Widget build(BuildContext context) { return widget.child; // ← gibt einfach child zurĂŒck, kein Error Handling! } } ``` **Problem:** Das "Error Boundary" hat keinen Error-Catcher (`FlutterError.onError` oder `ErrorWidget.builder` oder `runZonedGuarded`). Es fĂ€ngt genau null Fehler. Wird aber zum GlĂŒck auch nicht verwendet (der `AppWrapper` wird nie benutzt). **Fix:** Entweder korrekt implementieren (mit `runZonedGuarded` oder `ErrorWidget.builder`) oder — besser — den ganzen `AppWrapper` + `FlutterErrorBoundary` rauswerfen (Ponytail!). --- ## 📋 Schwere Probleme ### 🟠 [MAJ-1] Download-Fehlermeldungen werden brutal abgeschnitten **Datei:** `services/download_service.dart` · Zeilen 56, 75, 105, 127 ```dart _fehlermeldung = 'Timeout/Fehler bei Video-Info: ${e.toString().substring(0, 80)}'; ``` **Problem:** `.substring(0, 80)` und `.substring(0, 100)` zerstören Fehlerdetails. YouTube-Fehler (Rate Limits, Region-Locks, Copyright Claims) werden sinnlos. **Fix:** Entweder gar nicht truncaten, oder erst auf UI-Ebene truncaten. Die Fehlermeldung sollte vollstĂ€ndig geloggt werden: ```dart debugPrint('Download-Fehler (vollstĂ€ndig): $e'); _fehlermeldung = e.toString(); // UI zeigt nur ersten Teil, Log hat alles ``` --- ### 🟠 [MAJ-2] MiniPlayer `dispose()` tut nichts (lebt als Memory Leak II) **Datei:** `widgets/mini_player.dart` · Zeile 131 Kein `dispose()`-Override vorhanden! Die Klasse endet mit `Widget _playBtn() { ... }`. Dadurch werden nicht nur Streams nicht gecancelled (CRIT-1), sondern auch keine Ressourcen freigegeben. --- ### 🟠 [MAJ-3] `_scanneViaMediaStore()` gibt IMMER `[]` zurĂŒck **Datei:** `services/musik_scanner.dart` · Zeilen 121–126 ```dart Future> _scanneViaMediaStore() async { // Nutzt Android's MediaStore Query // Wird ĂŒber Method Channel in native Android implementiert // FĂŒr v1: Fallback auf Dateisystem-Suche return []; // ← TODO seit Version 1 } ``` **Problem:** Der MediaStore-Pfad ist nie implementiert. Der Fallback auf Dateisystem-Suche ist ineffizient und findet keine Musik in App-spezifischen Verzeichnissen. Auf Android 11+ (API 30+) wird der Dateisystem-Zugriff zudem stark eingeschrĂ€nkt. --- ### 🟠 [MAJ-4] `_p.positionStream` referenziert möglicherweise alten Player **Datei:** `services/player_service.dart` · Zeilen 32–33 ```dart Stream get positionStream => _p.positionStream; // _p initiiert lazy Stream get stateStream => _p.playerStateStream; ``` **Problem:** `_p` initialisiert den `AudioPlayer` lazy beim ersten Zugriff. Wenn die Streams vor dem ersten `spiele()`-Aufruf abonniert werden, hĂ€ngen sie an einem Player, der spĂ€ter ersetzt/reinitialisiert werden könnte. Der getter erzeugt keinen neuen Player, also ist das nur ein Problem wenn `_player` jemals auf null gesetzt wird (was nie passiert). Aber: `_p` wird von diesen Gettern aufgerufen → bei jedem Stream-Zugriff wird der `_player != null` Check gemacht. Das ist ok, aber es ist ein wartungsintensives Pattern. --- ### 🟠 [MAJ-5] `AppWrapper` ist totes Code-Gewebe **Datei:** `main.dart` · Zeilen 34–46 `AppWrapper` wird nirgends verwendet. `MeloApp` geht direkt zu `Scaffold(body: MeloHome())`. Der Wrapper wurde offenbar geplant aber nie aktiviert. --- ### 🟠 [MAJ-6] Tag-Filter wird in Song-Liste komplett ignoriert **Datei:** `screens/home_screen.dart` · Zeilen 374–393 + 431–434 `_aktiverTag` wird zwar gesetzt (Zeile 382), aber in `_songListe()` (Zeile 431–434) wird die Song-Liste ungefiltert angezeigt: ```dart itemCount: _songs.length, // ← immer alle Songs, egal welcher Tag aktiv ``` Die `_beispielTags` sind zudem hardcoded (nicht aus der DB) — das Tag-System hat keinen echten Filter. --- ## 📋 Code-QualitĂ€t & Anti-Patterns ### 🟡 [QUAL-1] Singleton-Overkill (5 von 6 Klassen sind Singletons) | Klasse | Singleton? | Warum problematisch | |--------|-----------|---------------------| | `DbHelper` | ✅ | Testen unmöglich (kein Mock) | | `PlayerService` | ✅ | State hĂ€lt zwischen Tests | | `FavoritenService` | ✅ | s.o. | | `DownloadService` | ✅ | s.o. | | `MusikScanner` | ✅ | s.o. | **Fix:** Dependency Injection via Konstruktor. Services sollten das DB-Objekt injiziert bekommen statt `DbHelper()` direkt aufzurufen. --- ### 🟡 [QUAL-2] HomeScreen Monolith — 519 Zeilen in einer Datei **Datei:** `screens/home_screen.dart` · 519 Zeilen EnthĂ€lt: - State Management - Header-Widget - Statistik-Widget - Tag-Leiste - Song-Liste - Song-Tile - Bottom Nav - Download-Dialog (komplette UI + Timer-Logik!) - Such-Dialog (komplette UI!) - Scan-Logik - Beispieldaten/Seed-Daten **Ponytail-Prinzip verletzt!** Max 500 Zeilen pro Datei fast erreicht, aber die Verantwortlichkeiten sind vermischt. **Fix:** - Extrahiere `MeloHeader`, `StatistikCard`, `TagLeiste`, `SongTile` in eigene Widget-Dateien (`widgets/`) - Extrahiere Such- und Download-Dialoge in eigene Methoden oder Dateien - Entferne Beispiel-Song-Seeding (gehört in einen dev-only Seeder) --- ### 🟡 [QUAL-3] `loeschen()` löscht in falscher Reihenfolge **Datei:** `database/db_helper.dart` · Zeilen 221–229 ```dart await d.delete('wiedergabe_verlauf'); await d.delete('song_tags'); await d.delete('playlist_songs'); await d.delete('playlists'); await d.delete('tags'); await d.delete('songs'); ``` **Problem:** Die Tabellen mit `ON DELETE CASCADE` werden vor den Eltern-Tabellen gelöscht. Theoretisch korrekt (CASCADE ist auf FK definiert), aber die Reihenfolge ist inkonsistent: `playlists` wird vor `tags` gelöscht, obwohl beide Eltern sind. Außerdem: **keine Transaktion!** Wenn ein Löschen fehlschlĂ€gt, hat man eine korrupte Datenbank. **Fix:** Alles in eine Transaktion packen: ```dart await d.transaction((txn) async { await txn.delete('wiedergabe_verlauf'); await txn.delete('song_tags'); await txn.delete('playlist_songs'); await txn.delete('playlists'); await txn.delete('tags'); await txn.delete('songs'); }); ``` --- ### 🟡 [QUAL-4] Doppelter try-catch in `musik_scanner.dart` **Datei:** `services/musik_scanner.dart` · Zeilen 29–65 Zwei verschachtelte try-catch Blöcke die fast identischen Code enthalten. Der Ă€ußere try-catch fĂ€ngt alles, und der innere ist ein "vielleicht klappt es beim zweiten Versuch" — ohne ersichtlichen Grund. **Fix:** Einfach einen try-catch: ```dart try { final file = File(pfad); final stat = await file.stat(); gefunden.add(/* Song erstellen */); } catch (e) { debugPrint('Datei nicht lesbar: $pfad — $e'); } ``` --- ### 🟡 [QUAL-5] `favoritenIds()` ist ineffizient (lĂ€dt alle Songs nur fĂŒr IDs) **Datei:** `services/favoriten_service.dart` · Zeilen 45–49 ```dart Future> favoritenIds() async { final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!); return songs.where((s) => s.id != null).map((s) => s.id!).toSet(); } ``` **Problem:** LĂ€dt komplette Song-Objekte (mit allen Feldern) aus der DB, nur um die IDs zu bekommen. Bei 1000 Songs werden 1000 `Song.fromMap()`-Aufrufe gemacht. **Fix:** Dedizierte DB-Query: ```dart Future> favoritenIds() async { final d = await _db.db; final rows = await d.rawQuery( 'SELECT song_id FROM playlist_songs WHERE playlist_id = ?', [_favoritenPlaylistId], ); return rows.map((r) => r['song_id'] as int).toSet(); } ``` --- ## 📋 ÜberschĂŒssiger Code (Ponytail-Prinzip) ### đŸ§č [PONY-1] 4 redundante Zeilen im `_p` Getter **Datei:** `services/player_service.dart` · Zeilen 16–25 ```dart AudioPlayer get _p { if (_player == null) { try { _player = AudioPlayer(); } catch (e) { debugPrint('AudioPlayer Init Fehler: $e'); _player = AudioPlayer(); // ← gleicher Code nochmal?? } } return _player!; } ``` Der doppelte `AudioPlayer()`-Aufruf im catch-Block ist sinnlos. Wenn der erste fehlschlĂ€gt, schlĂ€gt der zweite auch fehl. Einfach: ```dart AudioPlayer get _p => _player ??= AudioPlayer(); ``` --- ### đŸ§č [PONY-2] Beispiel-Songs + Tags aus home_screen.dart raus **Datei:** `screens/home_screen.dart` · Zeilen 32–40, 52–67 Die `_beispielTags` (hardcoded) und die Beispiel-Song-Seeding (Zeilen 53–67) gehören nicht in den produktiven Screen. Das ist Dev-Code. **Fix:** Entweder in einen `DevDataSeeder()` auslagern oder per `--dart-define` steuern. --- ### đŸ§č [PONY-3] Download-Dialog als StatefulBuilder im HomeScreen **Datei:** `screens/home_screen.dart` · Zeilen 183–245 Der komplette Download-Dialog mit Timer-Polling ist im HomeScreen vergraben. Das sind ~60 Zeilen UI + Logik. **Fix:** Eigene Widget-Datei `widgets/download_dialog.dart` mit `state`-haltendem Widget, das den Timer managed. --- ### đŸ§č [PONY-4] `_header()` + `_statistik()` + `_tagLeiste()` → eigene Widgets **Datei:** `screens/home_screen.dart` · Zeilen 280–393 Diese drei Methoden sind alle UI-only und könnten als eigene Widgets in `widgets/` leben. --- ## 📋 Security & Robustheit ### 🔒 [SEC-1] `Permission.storage` ist auf Android 33+ deprecated **Datei:** `services/musik_scanner.dart` · Zeile 19 ```dart final status = await Permission.storage.request(); ``` **Problem:** Ab API 33 (Android 13) gibt es `READ_MEDIA_AUDIO` statt `READ_EXTERNAL_STORAGE`. `Permission.storage` fragt nach der alten Permission, die auf neueren GerĂ€ten ignoriert wird. **Fix:** ```dart import 'dart:io' show Platform; // Oder besser ĂŒber permission_handler Android-specific await Permission.audio.request(); ``` --- ### 🔒 [SEC-2] Kein Retry-Mechanismus bei Netzwerkfehlern **Datei:** `services/download_service.dart` · Zeilen 32–131 Bei Timeout oder Verbindungsabbruch wird einfach `null` zurĂŒckgegeben. Kein Retry, kein exponentielles Backoff. **Fix:** 2-3 Retry-Versuche mit steigendem Timeout: ```dart for (int versuch = 0; versuch < 3; versuch++) { try { return await _downloadAttempt(url); } catch (e) { if (versuch == 2) rethrow; await Future.delayed(Duration(seconds: 2 * (versuch + 1))); } } ``` --- ### 🔒 [SEC-3] Kein Cancel-Mechanismus beim YouTube-Download **Datei:** `services/download_service.dart` · Zeilen 90–108 Einmal gestartet, kann der Download nicht abgebrochen werden. Der Nutzer muss warten oder die App killen. **Fix:** `StreamSubscription` speichern und `cancel()` anbieten: ```dart StreamSubscription? _downloadSub; void cancelDownload() => _downloadSub?.cancel(); ``` --- ## 📋 Missing Features ### 📌 [FEAT-1] **4 von 5 Bottom-Nav-Tabs sind leer** `home_screen.dart` Zeilen 509–515: Navigation hat 5 EintrĂ€ge, aber keine `_selectedIndex` und keine `IndexedStack` oder `switch`-Logik. Alle Tabs außer "Musik" zeigen denselben Screen. **Zu implementieren:** - `Downloads` → Zeige heruntergeladene Songs + Download-Buttons - `Tags` → Tag-Verwaltung (erstellen, löschen, Songs zuweisen) - `Favoriten` → Zeige Favoriten-Playlist - `Einstellungen` → Theme, Cache, Info --- ### 📌 [FEAT-2] **Keine Audio-Service Integration** `audio_service: ^0.18.15` ist als Dependency eingetragen, aber wird nirgends importiert oder verwendet. `PlayerService` ist standalone ohne Background-Playback. --- ### 📌 [FEAT-3] **`setWarteschlange` ohne Automatische Wiedergabe** `player_service.dart` Zeilen 83–87: Die Warteschlange wird gesetzt, aber nach dem letzten Song stoppt die Wiedergabe (kein Loop, kein Shuffle, keine Queue-Weiterverarbeitung). --- ## 📋 Zusammenfassung: Priority-TODO-Liste ### 🔮 MUST FIX (sofort — vor Release) | # | Datei | Zeile | Issue | |---|-------|-------|-------| | 1 | `widgets/mini_player.dart` | 20-32 | **Memory Leak:** Streams nie gecancelled | | 2 | `widgets/metadaten_dialog.dart` | 88 | **Null-Crash:** `song.id!` kann crashen | | 3 | `services/musik_scanner.dart` | 38 | **`dauerSekunden: 0`** — alle Songs haben Dauer 0 | | 4 | `database/db_helper.dart` | 126-135 | **DB-Flut:** positionAktualisieren inserted immer neu | | 5 | `main.dart` | 48-62 | **`FlutterErrorBoundary`** tut nichts (entfernen oder fixen) | | 6 | `widgets/mini_player.dart` | 131 | **Kein `dispose()`** — Leak #2 | ### 🟠 SHOULD FIX (nĂ€chster Sprint) | # | Datei | Zeile | Issue | |---|-------|-------|-------| | 7 | `services/download_service.dart` | 56,75,105,127 | Fehlermeldungen abgeschnitten (substring) | | 8 | `services/musik_scanner.dart` | 121-126 | `_scanneViaMediaStore()` gibt `[]` zurĂŒck | | 9 | `main.dart` | 34-46 | `AppWrapper` totes Gewebe (entfernen) | | 10 | `screens/home_screen.dart` | 382 | Tag-Filter tut nichts | | 11 | `screens/home_screen.dart` | 32-40 | Hardcoded Beispiel-Tags (aus DB laden!) | | 12 | `services/download_service.dart` | 90-108 | Kein Cancel-Mechanismus | | 13 | `services/download_service.dart` | — | Kein Retry bei Netzwerkfehlern | | 14 | `services/musik_scanner.dart` | 19-20 | Permission.storage deprecated (API 33+) | | 15 | `screens/home_screen.dart` | 509-515 | 4/5 Bottom-Nav-Tabs leer | | 16 | `services/favoriten_service.dart` | 45-49 | `favoritenIds()` lĂ€dt unnötig alle Songs | ### 🟡 NICE TO IMPROVE (Code-QualitĂ€t) | # | Datei | Zeile | Issue | |---|-------|-------|-------| | 17 | `screens/home_screen.dart` | 1-519 | **Monolith** — in Einzeldateien aufteilen | | 18 | `services/player_service.dart` | 16-25 | Redundanter try-catch im `_p` Getter | | 19 | `database/db_helper.dart` | 221-229 | `loeschen()` ohne Transaktion | | 20 | `services/musik_scanner.dart` | 29-65 | Doppelter try-catch Block | | 21 | — | — | **Singleton-Overkill** → Dependency Injection | | 22 | `screens/home_screen.dart` | 183-245 | Download-Dialog im HomeScreen (auslagern) | | 23 | `services/player_service.dart` | 83-87 | setWarteschlange ohne Weiterschaltung | | 24 | — | — | `audio_service` nie verwendet (Background-Playback) | --- ## 📊 Statistik | Metrik | Wert | |--------|------| | **Dateien** | 11 (12 mit pubspec.yaml) | | **Gesamt-LOC (Dart)** | 1.613 | | **GrĂ¶ĂŸte Datei** | `home_screen.dart` — 519 Zeilen (32%) | | **Singleton-Klassen** | 5/6 (83%) | | **Kritische Bugs** | **6** | | **Schwere Probleme** | **6** | | **Code-QualitĂ€t** | **5** | | **Ponytail-Verletzungen** | **4** (→ extrahieren) | | **Security** | **3** | | **Fehlende Features** | **4** (davon 1 komplett leer) | --- > **Fazit:** Die App hat ein solides GrundgerĂŒst, aber leidet unter klassischen "Solo-Dev"-Problemen: Memory Leaks, Null-Safety-LĂŒcken, Singleton-Overkill und ein Monolith-Screen. Der grĂ¶ĂŸte Hebel ist die Aufteilung von `home_screen.dart` (→ 3-4 eigene Widgets) und das Fixen der 6 kritischen Bugs. Danach: MediaStore-Integration fĂŒr echte Song-Dauer und die 4 leeren Tabs befĂŒllen. > "Weniger Code, mehr Wirkung" — **Ponytail-Prinzip.** Der MiniPlayer könnte mit dispose-Fix und gekĂŒrztem Code locker 30 Zeilen verlieren. Der HomeScreen sollte bei ~250 Zeilen landen.