188 lines
6.4 KiB
Dart
188 lines
6.4 KiB
Dart
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<int> dauernSekunden) =>
|
|
dauernSekunden.fold<int>(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<int> lade(List<SubsonicSong> 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;
|
|
}
|
|
|
|
/// Nimmt einen einzelnen Server-Titel offline.
|
|
///
|
|
/// Bewusst über [lade]: das bringt Doppel-Lauf-Schutz, Verbindungsprüfung
|
|
/// und Fortschritts-Buchführung mit. Gibt zurück, ob der Titel neu
|
|
/// dazugekommen ist.
|
|
///
|
|
/// **Bekannte Einschränkung:** Liegt der Titel schon im Abspiel-Zwischen-
|
|
/// speicher (weil er gerade gehört wurde), wird er trotzdem neu geladen —
|
|
/// bis die Verdrängung greift, belegt er doppelten Platz. Der Gegenschutz
|
|
/// kostete einen eigenen Cache-Zugang in diesem Dienst; für drei Nutzer ist
|
|
/// das der schlechtere Tausch.
|
|
Future<bool> ladeEinzelnenTitel(SubsonicSong song) async =>
|
|
await lade([song]) > 0;
|
|
|
|
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<void> _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<bool> 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<void> entferneAlle() async {
|
|
await speicher.loescheAlle();
|
|
await db.vergissAlleDownloads();
|
|
notifyListeners();
|
|
}
|
|
}
|