v2.53.1 — MED-2: Neue Server-Songs werden nach Download in die lokale DB eingefügt

## MED-2 (Review-Fix, KRITISCH)
- sync_service.dart: Nach erfolgreichem cloud.download wird der Song jetzt
  MIT cloud_id + Titeldaten (Titel/Künstler/Dauer/Größe/istKorrupt aus der
  Server-Liste) in die lokale DB eingefügt — vorher fehlte der Song in der
  Bibliothek, songNachCloudId blieb null und jeder Sync lud alle 325 Songs
  erneut (≈ GB/Sync) und meldete sie fälschlich als „+neu“
- Neuer statischer Helfer `songAusServerMap` (testbar): baut den Song aus
  den handle_list-Feldern; ytUrl wird bewusst NICHT übernommen (v2.52-F2:
  Server bleibt YT-Link-Quelle)
- Zielpfad-Kollisionen abgesichert: existiert die Datei bereits (verwaister
  Download aus früherem Sync ohne DB-Eintrag), wird „ (2)“, „ (3)“ … an-
  gehängt — sonst scheiterte der Insert am UNIQUE-Constraint datei_pfad
This commit is contained in:
Dustin
2026-08-05 11:24:11 +02:00
parent 3fe20e1c7b
commit 2cd34d61dc
+48 -3
View File
@@ -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