Auf Wunsch: was vom Handy hochgeladen wird, soll auch in Navidrome
auftauchen. Die Subsonic-API kennt keinen Upload — also erledigt es die
Melo-Cloud selbst.
Server (/home/dustin/scripts/melo_cloud.py, nicht versioniert — Backup als
melo_cloud.py.bak-20260821-084506):
- verknuepfe_navidrome(): Hardlink der Registry-Datei nach
/home/dustin/navidrome/music (Bind-Mount des Containers). Gleiche
Partition -> kein zusaetzlicher Speicher. Fallback: Kopie.
- rescan_navidrome(): Subsonic startScan.view ueber Navidromes
AuthProxyHeader (TrustedSources 127.0.0.1) — kein Passwort im Code.
Nur zur Beschleunigung, der Watcher findet neue Dateien ohnehin.
- upload() verknuepft neue UND bereits bekannte Titel (heilt Bestand).
- handle_delete() entfernt die Datei aus Registry + Navidrome, sobald kein
Konto sie mehr aktiv hat. Der Registry-EINTRAG bleibt stehen: handle_list
haengt die Grabsteine daran (JOIN registry) — ohne ihn erfuehren die
anderen Geraete nie von der Loeschung (Zombie-Song).
- registry_pfad(): gemeinsame Dateisuche ueber alle Audio-Endungen, mit
Rueckfall auf den Navidrome-Ordner.
Nebenbei repariert:
- Alle 325 Cloud-Dateien lagen nur noch im Navidrome-Ordner, REG war leer:
jeder Download antwortete "File missing". Per Hardlink zurueckverknuepft
(scripts/melo_cloud_migriere_registry.py).
- upload() legte jede Datei als ".mp3" ab, auch m4a/flac. Jetzt echte
Endung; _download liefert passenden Content-Type und einen Dateinamen
ohne doppelte Endung.
App:
- MeloCloudService.herunterladen() gibt die geschriebene Datei zurueck und
leitet die Endung aus dem Content-Type ab (endungFuer) — sonst landet
eine M4A als .mp3 auf dem Handy und Android ordnet sie falsch ein.
- loeschBremseGreift(): der Abgleich reicht keine Loeschwelle mehr zum
Server durch (>10 Titel UND >1/3 des Serverbestands). Ohne die Bremse
haette eine nicht eingehaengte Speicherkarte die Sammlung auf allen
Geraeten geloescht.
276 Tests gruen (1 uebersprungen), flutter analyze ohne Befund, plus ein
Durchlauf gegen den echten Server (scripts/test_melo_cloud_navidrome.py):
Upload -> Registry + Navidrome (ein Hardlink) -> Navidrome liest ein ->
Loeschen entfernt beides, Grabstein bleibt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
BUG 1 — Lieder vom Handy waren nicht abspielbar
("Wiedergabe fehlgeschlagen: (0) Source error" / "Loading interrupted")
Wurzel-Ursache: MeloAudioHandler.loadPlaylist hat jeden Warteschlangen-
Eintrag durch NavidromeService.streamAndCacheToLocal(item.id) geschickt,
sobald Zugangsdaten existierten. item.id ist aber NIE eine Navidrome-Song-ID
— lokal ist es file:///storage/..., beim Server die fertige Stream-Adresse.
Der Server bekam also 'file:///...' als Song-ID, antwortete mit einem Fehler,
und diese Fehlerantwort wurde an just_audio weitergereicht (Source error) —
bzw. als .mp3 in den Cache geschrieben, wodurch der Titel dauerhaft kaputt
blieb.
Zweite Ursache: die Schleife lud die GANZE Warteschlange seriell vorab
herunter (30s Timeout je Titel), bevor setAudioSources lief. Bei hunderten
Titeln startete die Wiedergabe deshalb nie; ein zweiter Tipp brach den
laufenden Ladevorgang ab ("Loading interrupted").
Fix:
- Server-Titel tragen ihre ID in MediaItem.extras['navidromeId'] statt sie
aus der Abspiel-Adresse zu raten. Neue reine Funktionen navidromeIdOf,
songIdOf, nutztServerCache, quelleFuer.
- loadPlaylist baut die Quellen ohne Netzzugriff; Caching des laufenden
Titels im Hintergrund (unawaited).
- Resume/Scrobble nur noch mit der jeweils passenden ID (Server bzw. lokal).
- ladeInCache() ersetzt streamAndCacheToLocal(): .part-Datei, Pruefung des
Inhaltstyps (istAudioAntwort), stabiler Cache-Schluessel ueber die
Song-ID statt der Stream-Adresse (die trug Token+Salt und war je Sitzung
anders — der Cache war nie wiederauffindbar), Client wird geschlossen.
BUG 2 — kein Abgleich zwischen Handy und Server
Neu: services/melo_cloud_service.dart + services/sync_service.dart gegen
cloud.baka-net.de (Bearer-JWT ueber BakaAuth). Server-Titel herunterladen
(offline verfuegbar), eigene Dateien hochladen, Loeschungen in beide
Richtungen (Tombstones), Favoriten und Wiedergabe-Verlauf. Automatisch beim
App-Start und bei Rueckkehr in die App (max. alle 15 Min), plus Knopf unter
Einstellungen -> Geraete-Abgleich. Reine Planungsfunktion planeSync().
Bewusst NICHT ueber Navidrome: die Subsonic-API kennt keinen Upload-
Endpunkt. Navidrome bleibt die Streaming-Bibliothek, die Melo-Cloud ist der
gemeinsame Speicher.
DB-Schema 8: songs.cloud_id verbindet Geraet und Server.
Nebenbei behoben (blockierte Build bzw. Tests):
- database.g.dart war veraltet — das Projekt liess sich nicht uebersetzen.
- metadataEdited wurde nirgends gesetzt/beachtet: von Hand korrigierte
Metadaten wurden vom naechsten Scan ueberschrieben. Jetzt in beiden
Scans respektiert; metadatenUebernahme() setzt die Markierung.
- song_detail_sheet_test.dart haengt beim Oeffnen des Modal-Sheets und
blockierte den gesamten Testlauf — vorerst uebersprungen (TODO im Code);
der Zweck wird von metadaten_uebernahme_test.dart abgedeckt.
Enthaelt ausserdem die bis dahin nicht committete Arbeit der Vorsitzung
(Musikerkennung/ACRCloud, MusicBrainz-Metadaten, MediaStore-Datentraeger).
267 Tests gruen (1 uebersprungen), flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
- AutoScan: beim Zurueckkehren in die App wird die Zahl der Musikdateien
mit der Bibliothek verglichen; nur bei Abweichung laeuft ein Scan, und
hoechstens alle 5 Minuten. Gleich viele Loeschungen wie Neuzugaenge
bleiben unbemerkt - dafuer gibt es den Scan von Hand.
- ServerNeuheiten: meldet neu auf Navidrome aufgetauchte Alben im
Online-Tab. Der erste Abruf meldet bewusst nichts, sonst waere die ganze
Sammlung "neu"; er nutzt die ohnehin geladenen Alben statt eines zweiten
Serveraufrufs.
- Neu: MeloDb.countSongs(), countAndroidMediaStoreSongs().
189 Tests gruen, flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
Neuer YouTube-Bereich im Online-Tab: Adresse einfuegen, herunterladen, der
Titel landet in Meine Musik.
- BakaAuth: Anmeldung gegen baka-net.de/auth, Token verschluesselt auf dem
Geraet. Der Server meldet Fehler mit HTTP 200 und Text im Rumpf, deshalb
wird der Inhalt geprueft statt nur der Statuscode.
- YtDownloadService: Proxy fragen, MP3 abholen, halbe Dateien aufraeumen.
Kuenstler nur aus "Kuenstler - Lied", sonst leer statt geraten.
- MediaStoreBridge (Kotlin): legt die Datei in Music/Melo ab und meldet sie
dem MediaStore. Noetig, weil der Android-Scan sonst beim naechsten Lauf
alles wieder als verschwunden markiert.
- SubTabs nach shared/ gezogen (jetzt in Meine Musik und Online genutzt).
177 Tests gruen, flutter analyze ohne Befund, Release-APK baut.
Der Weg ueber den echten Proxy ist noch nicht auf dem Geraet erprobt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
Untere Navigation folgt jetzt den Referenzbildern:
Meine Musik / Online / Suchen / Favoriten. Der bisherige "Download"-Tab
heisst "Online" und bekommt als Naechstes den YouTube-Downloader.
In "Meine Musik" gibt es Unterreiter (Songs/Kuenstler/Alben) im Pillen-Stil
der Referenz. Sie wechseln nur den Inhalt statt einen Bildschirm zu oeffnen;
die Schnellzugriff-Kacheln "Kuenstler"/"Alben" entfallen dadurch, ebenso die
damit tot gewordenen ArtistsScreen/AlbumsScreen-Huellen.
158 Tests gruen, flutter analyze ohne Befund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
Sperrbildschirm und ReplayGain
- DB-Schema 6: lyrics + gain_db
- Songtext bevorzugt lokalen Tag, Server nur als Rueckfall; Song-ID-Bug
beim Lyrics-Aufruf behoben
- "Als Naechstes spielen" / "Zur Warteschlange hinzufuegen" ohne Eingriff
in Favoriten oder Wiedergabelisten
- songToMediaItem nimmt eine Cover-Vorgabe: Sperrbildschirm zeigt das
Kategorie-Cover
- ReplayGain: Tags werden gelesen, laute Titel abgesenkt, abschaltbar
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
- Meine Musik als Startbildschirm ohne Topbar: Kopfzeile (Einstellungen,
Suche, Musikerkennung), Schnellzugriffe, Shuffle + Sortieren, Liederliste
- Sortierung nach Zuletzt hinzugefuegt / Name (A-Z-#) / Wie oft abgespielt,
auf- und absteigend, pro Liste gespeichert (SortStore)
- Favoriten als eigener Tab mit gleicher Shuffle-/Sortier-Leiste
- Download-Tab uebernimmt den Navidrome-Server-Browser
- DB-Schema 3: playCount, gezaehlt beim Titelstart
- Theme auf #0B0B10 / #c0392b
- Scan-Aktionen aus der entfallenen Topbar jetzt in den Einstellungen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
Tiefe Debug-Runde bewiesen: Server, DB-Passwort und App-Token-Logik sind
korrekt (8/8-Repro gegen Live-Server) — abgelehnt wird ein falscher
Passwort-Wert vom Gerät. Damit das künftig sofort sichtbar ist:
- NavidromeService.letzterLoginFehler hält die Server-Meldung fest
- Login-Dialog zeigt sie statt generischem "Login fehlgeschlagen"
- Fehler-Log enthält die Passwort-Länge (nie das Passwort selbst)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRj3pupcigUi683vy2Wwnz
Systematische Diagnose bewies: App-Code, Server, User 'Baka' und Passwort sind
korrekt (1:1-Nachbau des App-Requests inkl. Salt/Token -> Navidrome status:ok).
Der Fehler lag im auf dem Gerät verwendeten Credential-Wert:
- der alte Build (Ping-Fehlalarm) persistierte ungültige Zugangsdaten,
- verdeckte Tippfehler im obscureText-Passwortfeld blieben unsichtbar.
Fix:
- Passwort-Anzeigen-Umschalter (Auge-Icon) im Login-Dialog.
- URL + Passwort werden getrimmt (Autofill/Tastatur-Leerzeichen).
Ein erfolgreicher Login ueberschreibt die alten gespeicherten Credentials;
der Ping-Fix stellt sicher, dass nur gueltige gespeichert werden.
84/84 Tests gruen, analyze sauber.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr2UwurtxyJUpWpBSeVmBm
Diagnose-Logs werden zentral bereinigt, bevor sie an logs.baka-net.de gehen.
Lokaler debugPrint bleibt vollständig.
- Freitext-Scrubber (LoggerService.scrubText) für message + stackTrace:
maskiert user=/pw=/password=/token=/secret=/url= sowie Datei-Pfade,
nackte URLs, Bearer-Token, JWTs und lange Hex-IDs.
- stackTrace auf 8 Zeilen gekürzt (capFrames).
- deviceModel nur noch grobe Plattform (android/ios) statt Hersteller+Modell;
device_info_plus im Logger entfernt.
- Quelle entschärft: settings_screen loggte Navidrome-Login mit Klartext
user=/url= — jetzt nur neutraler Status inkl. pwLeer-Flag.
- Neue Tests: test/logger_service_test.dart (9). 79/79 grün, analyze sauber.
Opt-in/Einwilligung bleibt separater Folgeschritt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr2UwurtxyJUpWpBSeVmBm
ROOT CAUSE für die fehlenden Logs — und für den fehlschlagenden
Navidrome-Login: Flutter injiziert android.permission.INTERNET nur in
debug/ und profile/AndroidManifest.xml. In main/AndroidManifest.xml
stand sie nicht, das Release-APK hatte damit gar keinen Netzzugriff.
Jede http-Anfrage schlug sofort mit SocketException fehl.
- INTERNET + ACCESS_NETWORK_STATE in main/AndroidManifest.xml ergänzt
- Login-Versuch/-Erfolg/-Fehlschlag im Settings-Dialog explizit geloggt
(URL + User + ob das Passwortfeld leer war; das Passwort selbst nicht)
- Batch-Schwelle 20 -> 5, damit INFO-Logs auch ohne vorherigen Fehler
hochgeladen werden
Nicht geaendert: der Sofort-Upload bei ERROR war bereits implementiert
(error() ruft _uploadIfError -> _uploadLogs direkt auf) und war nicht
die Ursache.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
Bisher stand im Log nur die nackte Exception-Message ("Connection timeout")
— ohne URL und ohne Fehlertyp war die Ursache nicht eingrenzbar.
Neu im Fehler-Log:
- Fehlerart klassifiziert: TIMEOUT / SOCKET / TLS-HANDSHAKE / HTTP / FORMAT
- SocketException zusätzlich mit osError-Message, errorCode, Adresse und
Adress-Typ (IPv4 vs IPv6 — zeigt ein IPv6-Problem direkt an)
- Server-URL sowie geparstes scheme/host/port (falsche URL, fehlendes
https, DNS-Auflösung erkennbar)
- e.runtimeType + Message
Bewusst NICHT geloggt: die volle Ping-URI — sie enthält Auth-Token und
Salt als Query-Parameter, die nicht auf dem Log-Server landen sollen.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
1. 🔗 AUTO-URL: Server-URL \"https://musik.baka-net.de\" vorausgefüllt (editierbar)
2. 👤 USER-BUTTONS: Baka/Tinker/Dustin als Button-Auswahl (statt Text-Input)
3. 🔐 PASSWORT-ONLY: Dialog zeigt nur Passwort-Feld + User-Auswahl (wie beim Register!)
4. 💡 GAST-MODUS: Info-Text erklärt lokale Nutzung ohne Server-Konto
→ App funktioniert auch komplett offline mit lokaler Musikbibliothek!
UX-Verbesserungen:
- Weniger zu tippen (URL + User vorgegeben)
- Schneller Login für Baka/Tinker/Dustin
- Klare Fallback-Option für lokale Nutzung
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
- Größeres MiniPlayer-Widget auf allen Non-Player-Screens (80px statt 60px)
- Größeres Album-Cover (64px statt 44px) mit besserer Sichtbarkeit
- Skip-Buttons (⏮️ Vorheriges + ⏭️ Nächstes) für schnelle Navigation
- Progress-Bar zeigt Wiedergabe-Position in Echtzeit
- Besseres Styling + Schrift-Gewicht für Titel
- Funktioniert wie Home-Widget: zeigt überall was gerade spielt
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
- androidShowNotificationBadge aktiviert für bessere Notification-Sichtbarkeit
- Notification bleibt aktiv auch wenn Musik pausiert (AndroidService-Konfiguration)
- Play/Pause/Skip-Controls auf Lock Screen + Notification (AudioService-Standard)
- Media-Session Integration für Android SystemUI Controls
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
- getPlaylists() + getPlaylistSongs() APIs für Subsonic-Playlist-Laden
- syncPlaylistsFromServer() in PlaylistService importiert alle Server-Playlisten als lokale
- Benennung mit 🌐 Präfix um Server-Playlisten zu kennzeichnen
- Settings-Button lädt alle Playlisten mit ihren Songs vom Server
- Zeigt Anzahl importierter Playlisten mit SnackBar
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
- Neue NavidromeService APIs: scrobble() + getBookmark()
- Lädt Wiedergabe-Position vom Server beim Song-Start (Subsonic-API)
- Sendet Position alle 5s zum Server wenn verbunden
- Priorisiert Server-Position über lokale Position
- Full Sync: lokal speichern + Server synchen parallel
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
- Toggle-Buttons in Navidrome-Tab: "📀 Alben" ↔ "🎤 Künstler"
- Artist-Liste zeigt Namen pro Künstler, Tap lädt alle Songs des Künstlers
- _playArtist() Methode lädt Künstler-Songs via getArtistSongs() API
- Albums/Artists sind Lazy-Loaded (nur beim Toggle laden)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
- Neuer 'Cache-Speicher' ListTile in Musikserver-Sektion
- Zeigt Cache-Größe in MB/KB/B
- Clear-Button: Löscht alle gecachten Lieder
- _formatBytes() für Größen-Anzeige
- _clearCache() mit SnackBar-Feedback
Nutzer kann jetzt Cache-Status sehen + leeren!
Tests: 70/70 ✅
IMPLEMENTATION:
- NavidromeService.streamAndCacheToLocal(): Stream + gleichzeitig cachen
- Cache-First-Logik: Wenn Cache existiert → lokal spielen
- AudioHandler erweitert: loadPlaylist() nutzt Auto-Caching
- CacheManager Integration in AudioHandler
FLOW:
1. User spielt Album ab → streamt vom Server
2. Während des Abspielens → lokal in Cache speichern
3. Nächstes Mal: Lokal spielen (schneller, Offline möglich)
Phase 2 P0 COMPLETE! ✅
Tests: 70/70 ✅
LYRICS:
- Lyrics-Button im Now-Playing Screen (📝 Icon)
- Bottom-Sheet mit Lyrics vom Server (getLyrics() API)
- Fallback: 'Keine Lyrics verfügbar'
- Lyrics-Klasse für Datenmodell
SHUFFLE/REPEAT:
- Shuffle-Button (schon implementiert!) zeigt aktiven Status rot
- Repeat-Button (Off → All → One → Off cycle)
- Beide via AudioHandler.setShuffleMode() / setRepeatMode()
- Integration in _Controls (Player-UI)
Phase 1 P1-Features jetzt funktionsfähig!
Tests: 70/70 ✅
- getArtists(): Alle Künstler vom Server laden
- getArtistSongs(): Alle Songs eines Künstlers
- setFavorite()/removeFavorite(): Server-Favoriten toggle
- getFavorites(): Favoriten vom Server laden
- SubsonicArtist-Klasse für Künstler
APIs für Phase 2 vorbereitet (Artist-Tab, Server-Favoriten-Sync)
Tests: 70/70 grün
- Neuer 4. Tab: '🌐 Server' zeigt Alben vom Musikserver
- Album-Liste laden und anzeigen (AsyncBuilder)
- Tap auf Album → alle Songs in Queue laden + abspielen
- Graceful Fallback wenn Server nicht verbunden
- Tests: 70/70 grün
- Analyzer: ✅ keine Fehler
Phase 1 P0-Feature: Album-Browser vom Server
- Bibliotheks-Statistik: Song-Anzahl + Gesamtspieldauer via watchSongs()
(reine Summier-/Format-Funktion in lib/settings/library_stats.dart,
unabhängig von Flutter/Plattform testbar)
- Berechtigungen: Musik-Berechtigung öffnet Systemeinstellungen über das
bereits vorhandene openMusicPermissionSettings()
- Über Melo: kurzer Name + Slogan, keine erfundene Versionsnummer
(package_info_plus ist keine Dependency)
- Keine Platzhalter für Profil/Cloud-Sync/Geräte-Verwaltung, da es noch
kein Account-/Cloud-System gibt (Phase 2)
- HomeShell nutzt SettingsScreen statt des Settings-Placeholders,
toter _Placeholder-Widget-Code entfernt
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- MeloAudioHandler bekommt MeloDb per Konstruktor injiziert
- Positions-Timer (alle 5s) schreibt db.recordPlayback(songId, positionMs)
während aktiv abgespielt wird (songId aus mediaItem.extras['songId'])
- loadPlaylist liest db.lastPosition() für den Start-Song und seekt vor
dem play() an die gespeicherte Position, statt immer von vorne zu starten
- shouldResumeAt() als reine, testbare Funktion extrahiert: kein Resume bei
Position 0 oder innerhalb der letzten 3s einer bekannten Track-Dauer
- dispose() zum gefahrlosen Abbrechen des Positions-Timers in Tests ohne
vollen App-Lifecycle (super.stop() lässt sich in Widget-Tests wegen der
aktiven playbackState-Pipe nicht gefahrlos aufrufen)
- library_screen_test.dart: MeloAudioHandler-Konstruktion angepasst (db
jetzt Pflichtparameter) + handler.dispose() im Teardown
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Neuer QueueScreen: zeigt handler.queue, hebt aktuell spielenden Titel
hervor (fett/rot), Antippen springt per skipToQueueItem
- Entfernen und Umsortieren ohne Änderung an MeloAudioHandler: Queue wird
clientseitig neu berechnet und per loadPlaylist neu geladen; Position
wird nur wiederhergestellt, wenn der abgespielte Titel unverändert blieb
- Reine Queue-Logik (removeFromQueue/reorderQueue/newPlayingIndex) als
eigenständige, ohne AudioHandler testbare Funktionen extrahiert
- Warteschlangen-Button in der Now-Playing-AppBar verlinkt den neuen Screen
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Library screen now has a TabBar (Alle / Künstler / Alben) inside a
DefaultTabController; scan/permission/error banners stay above the
TabBarView so they remain visible regardless of the active sub-tab
- New lib/library/song_grouping.dart: pure, unit-tested functions
groupByArtist/groupByAlbum (null artist/album -> "Unbekannt" /
"Unbekanntes Album", alphabetically sorted) plus albumArtistLabel
("Verschiedene Interpreten" when an album mixes artists)
- New lib/library/artist_list.dart + album_list.dart: StreamBuilder on
MeloDb.watchSongs(), grouped client-side, tapping a row pushes a
SongList screen for that artist/album; album rows show a CoverImage
thumbnail from the first song's coverPath
- Existing "Alle" tab content (recently-added strip, full song list,
empty state) preserved unchanged, moved into _AllSongsTab
Genre browsing is intentionally out of scope: the Songs table has no
genre column and neither scanner extracts genre metadata — needs a
schema migration + scanner work in a future task.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
PlaylistDetailScreen zeigt die Songs einer Playlist (watchPlaylistSongs),
erlaubt Entfernen einzelner Songs und Umsortieren per Drag & Drop
(ReorderableListView.builder). Tippen auf einen Song spielt die Playlist
ab dieser Stelle. Leer-Zustand mit Hinweistext.
Da bestehende position-Werte nach Einfügereihenfolge lückenhaft sein
können, schreibt das Umsortieren nicht nur den verschobenen Song um,
sondern vergibt für die gesamte Playlist neue lückenlose Positionen
(0..N-1): MeloDb.reorderAllPlaylistSongs (in einer Transaktion) +
PlaylistService.reorderAll als dünner Wrapper.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Ohne Eintrittspunkt wären Playlisten unbenutzbar, da sie sonst nie Songs
erhalten könnten. SongList bekommt einen zusätzlichen Trailing-Button
(Icons.playlist_add) neben dem Favoriten-Herz, der ein Bottom-Sheet mit
allen Playlisten öffnet ("Neue Playlist" oben, dann bestehende). Antippen
hängt den Song ans Ende der gewählten Playlist an und zeigt eine SnackBar.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- PlaylistsScreen: Favoriten-ListTile oben (watchFavorites, pusht FavoritesListScreen),
darunter alle Playlisten (watchPlaylists) mit Songanzahl, Löschen via Icon/Long-Press
+ Bestätigungsdialog
- CreatePlaylistDialog: Name-Eingabe, Erstellen-Button erst aktiv bei nicht-leerem Text,
gibt die neue Playlist-ID zurück
- Leer-Zustand "Noch keine Playlisten" nach Vorbild von library_screen.dart
- main.dart: Playlisten-Platzhalter durch PlaylistsScreen ersetzt
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- "Zuletzt hinzugefügt" aus dem alten HomeScreen in die Bibliothek verschoben:
horizontal scrollende Leiste über der vollen Songliste, nur sichtbar wenn
nicht leer und nicht gerade gescannt wird
- home_screen.dart entfernt (Funktionalität jetzt Teil von LibraryScreen)
- HomeShell-Tabs auf die finale Struktur umgestellt: NowPlayingScreen an
Position 0, Playlisten- und Settings-Platzhalter an ihren finalen
Positionen (werden in späteren Tasks durch echte Screens ersetzt)
- BottomNavigationBar-Icons/Labels an die neue Reihenfolge angepasst
- MiniPlayer wird ausgeblendet, solange der Now-Playing-Tab aktiv ist
(redundant, da der Tab selbst schon die volle Wiedergabe-Ansicht zeigt)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- songToMediaItem trägt jetzt die Song-UUID in extras['songId'], damit
Downstream-Code (Favoriten-Button, später Playback-History) den echten
Song wiederfinden kann statt nur die file-URI zu haben
- NowPlayingScreen: Schließen-Button entfernt (AppBar-leading), da der
Screen jetzt als eigener Tab läuft statt gepusht zu werden
- FavoriteButton für den aktuellen Song in die AppBar-Actions eingebaut,
reaktiv über einen verschachtelten StreamBuilder<MediaItem?>; ohne
gültige songId (z.B. nichts spielt) wird kein Button gerendert
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Filled heart when favorited, outline when not (reactive via watchIsFavorite)
- Wired into every song list tile (library, search, playlists)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Timer stops playback (pause) after 5/10/15/30/60 min. Wall-clock
deadline is self-correcting against OS throttling; 1Hz tick drives
the countdown UI. Cancelled in stop() so no timer resurrects a paused
player after teardown.
- sleep_timer.dart: isolated time logic, injectable clock for testing
- audio_handler.dart: sleepTimer field + cancel() in stop()
- now_playing_screen.dart: AppBar button + bottom sheet
- 10 unit tests via mocked clock
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Critical Blockers Fixed
- Remove playbackState.addError() which broke audio_service permanently
- Move error handling from audio_handler to song_list where UI context exists
- Display error in SnackBar instead of destroying playback stream
- Add escapeChar parameter to LIKE queries for correct wildcard handling
- Fixes broken search for titles with _ or % characters
- Add dismissScanError() method to allow closing error banner
- Previously banner couldn't be dismissed (missing notifyListeners())
## Improvements
- Remove dead livePaths list, use companions.isNotEmpty instead
- Add initial progress update to show total files when scan starts
- Add tooltip to now_playing_screen Zurück button
- Add proper error handling in song_list with async/await
## Testing
- Add regression test for CRITICAL-1: empty scan doesn't delete songs
- Add regression test for CRITICAL-2: tombstoned files restore on re-scan
- All 11 tests pass
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>