diff --git a/lib/services/sync_service.dart b/lib/services/sync_service.dart index ccfe5a2..de2d933 100644 --- a/lib/services/sync_service.dart +++ b/lib/services/sync_service.dart @@ -153,6 +153,38 @@ class SyncService { return letzterSync == null || d == null || d.isAfter(letzterSync); } + /// Baut den lokalen [Song]-DB-Eintrag aus einem Server-Listeneintrag + /// (handle_list-Felder: id/title/artist/duration/size/istKorrupt). + /// + /// Grundlage des MED-2-Fixes: Neue Server-Songs werden nach dem Download + /// MIT cloud_id + Titeldaten in die lokale DB eingefügt — vorher fehlten + /// sie in der Bibliothek und wurden bei jedem Sync erneut heruntergeladen. + /// `ytUrl` wird bewusst NICHT übernommen (v2.52-F2: Der Server ist die + /// Quelle für YT-Links, lokal wird yt_url nicht mehr persistiert). + /// Reine, statische Funktion → testbar. + @visibleForTesting + static Song songAusServerMap(Map song, String destPath) { + final dauerRaw = song['duration']; + final dauer = dauerRaw is num + ? dauerRaw.toInt() + : int.tryParse(dauerRaw?.toString() ?? '') ?? 0; + final groesseRaw = song['size']; + final groesse = groesseRaw is num + ? groesseRaw.toInt() + : int.tryParse(groesseRaw?.toString() ?? '') ?? 0; + return Song( + titel: (song['title'] ?? 'unknown').toString(), + kuenstler: (song['artist']?.toString() ?? '').trim(), + dauerSekunden: dauer, + groesseBytes: groesse, + dateiPfad: destPath, + downloadQuelle: 'cloud', + istHeruntergeladen: true, + istKorrupt: song['istKorrupt'] == true, + cloudId: song['id']?.toString(), + ); + } + /// Lokale Löschung eines Cloud-Songs an den Server melden (Tombstone). /// Der Server speichert `deleted=1` + `deleted_at` → andere Geräte löschen /// den Song nach ihrem nächsten Sync ebenfalls lokal (keine Zombie-Songs). @@ -351,13 +383,26 @@ class SyncService { onFortschritt?.call('Download: $title…', 0.1 + (0.4 * processed / (totalNew > 0 ? totalNew : 1))); final safeTitle = sanitizeDateiname(title); - final dest = '${dir.path}/$safeTitle'; + // Eindeutiger Zielpfad: Kollisionen (gleichnamiger Titel oder + // verwaiste Datei aus einem früheren Sync ohne DB-Eintrag) würden + // die Datei überschreiben bzw. am UNIQUE-Constraint der + // datei_pfad-Spalte scheitern (Insert würde still ignoriert). + var dest = '${dir.path}/$safeTitle'; + var kollision = 2; + while (await File(dest).exists()) { + dest = '${dir.path}/$safeTitle ($kollision)'; + kollision++; + } if (await cloud.download(sid, dest)) { downloaded++; neueSongs++; neueDetails.add('+ Neuer Song: $title'); - // In DB eintragen mit cloud_id - // (vereinfacht: ID3-Reader würde Titel extrahieren) + // MED-2: Song mit cloud_id + Titeldaten aus der Server-Liste in + // die lokale DB einfügen — sonst fehlt der Song in der + // Bibliothek, songNachCloudId bleibt null und der Song wird bei + // JEDEM Sync erneut heruntergeladen (GB/Sync) und fälschlich als + // „+neu“ gemeldet. + await _db.songEinfuegen(songAusServerMap(song, dest)); } await Future.delayed( const Duration(milliseconds: 50)); // UI-Update erlauben