import 'package:drift/drift.dart' show Value; import 'package:flutter/foundation.dart'; import '../library/database.dart'; import 'download_speicher.dart'; import 'navidrome_service.dart'; /// Ab wie vielen Titeln vor dem Herunterladen nachgefragt wird. /// /// Ein Album sind zehn bis zwanzig Titel — da wäre eine Rückfrage nur im Weg. /// Ein Künstler können mehrere hundert und einige Gigabyte sein, und das /// sollte niemand aus Versehen antippen. const rueckfrageAb = 30; bool brauchtRueckfrage(int anzahl) => anzahl >= rueckfrageAb; /// Grobe Größenschätzung für die Rückfrage. Genau geht nicht — der Server /// verrät die Dateigröße erst beim Abruf; diese Zahl beruht auf der Dauer /// bei rund 256 kbit/s und ist als Hausnummer gedacht. int geschaetzteBytes(Iterable dauernSekunden) => dauernSekunden.fold(0, (summe, s) => summe + s * 32000); enum _Ergebnis { neu, schonDa, fehler } /// Lädt Server-Titel bewusst auf das Gerät und führt Buch darüber. /// /// Getrennt vom Zwischenspeicher, der beim Abspielen nebenbei entsteht: was /// hier landet, hat der Nutzer gewollt und wird nie von selbst verdrängt. class DownloadService extends ChangeNotifier { DownloadService({ required this.db, required this.navidrome, DownloadSpeicher? speicher, }) : speicher = speicher ?? DownloadSpeicher(); final MeloDb db; final NavidromeService navidrome; final DownloadSpeicher speicher; bool _laeuft = false; int _fertig = 0; int _gesamt = 0; int _fehlgeschlagen = 0; String? _aktuell; String? _fehler; bool _abbruchGewuenscht = false; bool get laeuft => _laeuft; int get fertig => _fertig; int get gesamt => _gesamt; /// Titel, der gerade geladen wird. String? get aktuell => _aktuell; /// Meldung des letzten Fehlschlags. Sagt **nicht**, dass der ganze Lauf /// gescheitert ist — siehe [fehlgeschlagen]. String? get fehler => _fehler; /// Wie viele Titel im letzten Lauf nicht geladen werden konnten. int get fehlgeschlagen => _fehlgeschlagen; /// Fortschritt zwischen 0 und 1, oder `null`, solange nichts läuft. double? get anteil => _laeuft && _gesamt > 0 ? (_fertig / _gesamt).clamp(0.0, 1.0) : null; void abbrechen() { if (_laeuft) _abbruchGewuenscht = true; } /// Lädt [songs] herunter. Bereits vorhandene Titel werden übersprungen. /// Gibt zurück, wie viele neu dazugekommen sind. Future lade(List songs) async { if (_laeuft) return 0; // Die Zugangsdaten können sich seit dem App-Start geändert haben: wer // sich erst in den Einstellungen anmeldet, hätte hier sonst eine leere // Verbindung und jeder Download liefe ins Leere. if (!navidrome.istVerbunden) { await navidrome.ladeGespeicherteZugangsdaten(); } if (!navidrome.istVerbunden) { _fehler = 'Nicht mit dem Musikserver verbunden'; notifyListeners(); return 0; } _laeuft = true; _abbruchGewuenscht = false; _fehler = null; _fehlgeschlagen = 0; _fertig = 0; _gesamt = songs.length; notifyListeners(); var neu = 0; try { for (final song in songs) { if (_abbruchGewuenscht) break; _aktuell = song.titel; notifyListeners(); final ergebnis = await _ladeEinen(song); if (ergebnis == _Ergebnis.neu) neu++; if (ergebnis == _Ergebnis.fehler) _fehlgeschlagen++; _fertig++; notifyListeners(); } } finally { _laeuft = false; _aktuell = null; notifyListeners(); } return neu; } Future<_Ergebnis> _ladeEinen(SubsonicSong song) async { try { // Datei und Buchführung entstehen nacheinander. Bricht die App // dazwischen ab, liegt eine Datei ohne Zeile herum — sie tauchte dann // in keiner Liste auf und ließe sich nie wieder loswerden. Deshalb hier // beides prüfen und die fehlende Zeile nachtragen, statt nur „ist schon // da" zu melden. final vorhanden = await speicher.treffer(song.id); if (vorhanden != null) { await _buchen(song, await vorhanden.length()); return _Ergebnis.schonDa; } final ziel = await speicher.ziel(song.id); final datei = await navidrome.ladeDateiHerunter(navidrome.streamUrl(song.id), ziel); if (datei == null) { _fehler = 'Nicht ladbar: ${song.titel}'; return _Ergebnis.fehler; } await _buchen(song, await datei.length()); return _Ergebnis.neu; } catch (e) { _fehler = 'Fehler bei ${song.titel}: $e'; debugPrint('Download fehlgeschlagen: $e'); return _Ergebnis.fehler; } } Future _buchen(SubsonicSong song, int bytes) => db.merkeDownload(DownloadsCompanion.insert( navidromeId: song.id, titel: song.titel, kuenstler: Value(song.kuenstler.isEmpty ? null : song.kuenstler), album: Value(song.album.isEmpty ? null : song.album), groesseBytes: bytes, geladenAmMs: DateTime.now().millisecondsSinceEpoch, )); /// Nimmt einen heruntergeladenen Titel wieder vom Gerät. /// Nimmt einen heruntergeladenen Titel wieder vom Gerät. /// /// Die Zeile fällt nur, wenn die Datei wirklich weg ist — sonst bliebe eine /// Datei ohne Buchführung liegen, die in keiner Liste mehr auftaucht. Future entferne(String navidromeId) async { await speicher.loesche(navidromeId); if (await speicher.treffer(navidromeId) != null) { _fehler = 'Datei ließ sich nicht entfernen'; notifyListeners(); return false; } await db.vergissDownload(navidromeId); notifyListeners(); return true; } Future entferneAlle() async { await speicher.loescheAlle(); await db.vergissAlleDownloads(); notifyListeners(); } }