Automatische Erkennung: neue Dateien auf dem Geraet und neue Alben am Server
- 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
This commit is contained in:
co-authored by
Claude Opus 5
parent
6200c77614
commit
15e3d44453
@@ -5,6 +5,32 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 🔎 Melo merkt selbst, wenn neue Musik da ist (2026-08-20)
|
||||||
|
- 📂 **Neue Dateien auf dem Handy:** Kommst du in die App zurück, sieht Melo
|
||||||
|
nach, ob sich der Musikbestand geändert hat — und liest neue Titel von
|
||||||
|
allein ein. Musik per Kabel überspielt oder anderswo heruntergeladen? Sie
|
||||||
|
ist einfach da, ohne dass du "Musik scannen" antippen musst.
|
||||||
|
🔧 **Wie sparsam das ist:** Melo vergleicht nur die **Anzahl** der Dateien
|
||||||
|
und prüft höchstens alle 5 Minuten. Der eigentliche Scan läuft nur, wenn
|
||||||
|
die Zahl abweicht — sonst passiert gar nichts.
|
||||||
|
⚠️ **Grenze, ehrlich gesagt:** Werden genauso viele Titel gelöscht wie
|
||||||
|
hinzukommen, bleibt die Zahl gleich und Melo merkt es nicht. Dafür gibt es
|
||||||
|
weiterhin den Knopf zum Scannen von Hand.
|
||||||
|
- 🌐 **Neues auf dem Musikserver:** Im Tab **Online → Server** erscheint ein
|
||||||
|
Hinweis, wenn auf Navidrome Alben dazugekommen sind ("3 neue Alben auf dem
|
||||||
|
Server"). Ein Tipp auf *Verstanden* und der Hinweis ist weg — dieselben
|
||||||
|
Alben melden sich nicht noch einmal.
|
||||||
|
🔧 Beim allerersten Mal meldet Melo bewusst **nichts**: sonst wäre deine
|
||||||
|
ganze Sammlung auf einmal "neu". Melo merkt sich still den Stand und
|
||||||
|
meldet ab dann nur noch, was wirklich dazukommt. Der Hinweis kostet keinen
|
||||||
|
zusätzlichen Serveraufruf — er nutzt die Alben, die ohnehin geladen werden.
|
||||||
|
- 🔧 **Details:**
|
||||||
|
- Neu: `library/auto_scan.dart` (6 Tests), `services/server_neuheiten.dart`
|
||||||
|
(5 Tests), `MeloDb.countSongs()` (1 Test),
|
||||||
|
`countAndroidMediaStoreSongs()`.
|
||||||
|
- `HomeShell` hört jetzt auf den App-Lebenszyklus (`resumed`).
|
||||||
|
- ✅ **Getestet:** 189 Tests grün (12 neue), `flutter analyze` ohne Befund.
|
||||||
|
|
||||||
### ⬇️ YouTube-Downloader mit Baka-Anmeldung (2026-08-20)
|
### ⬇️ YouTube-Downloader mit Baka-Anmeldung (2026-08-20)
|
||||||
- 🎬 **Musik von YouTube holen:** Im Tab **Online → YouTube** eine Adresse
|
- 🎬 **Musik von YouTube holen:** Im Tab **Online → YouTube** eine Adresse
|
||||||
einfügen, auf *Herunterladen* tippen — fertig. Der Titel landet danach
|
einfügen, auf *Herunterladen* tippen — fertig. Der Titel landet danach
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import '../player/audio_handler.dart';
|
|||||||
import '../services/baka_auth.dart';
|
import '../services/baka_auth.dart';
|
||||||
import '../services/media_store.dart';
|
import '../services/media_store.dart';
|
||||||
import '../services/navidrome_service.dart';
|
import '../services/navidrome_service.dart';
|
||||||
|
import '../services/server_neuheiten.dart';
|
||||||
import '../services/yt_download_service.dart';
|
import '../services/yt_download_service.dart';
|
||||||
import '../shared/sub_tabs.dart';
|
import '../shared/sub_tabs.dart';
|
||||||
|
import '../shared/theme.dart';
|
||||||
|
|
||||||
/// Online-Tab: durchsucht den verbundenen Navidrome-Server. Abgespielte
|
/// Online-Tab: durchsucht den verbundenen Navidrome-Server. Abgespielte
|
||||||
/// Titel werden vom [MeloAudioHandler] automatisch lokal zwischengespeichert
|
/// Titel werden vom [MeloAudioHandler] automatisch lokal zwischengespeichert
|
||||||
@@ -63,6 +65,9 @@ class _ServerBrowser extends StatefulWidget {
|
|||||||
|
|
||||||
class _ServerBrowserState extends State<_ServerBrowser> {
|
class _ServerBrowserState extends State<_ServerBrowser> {
|
||||||
late final NavidromeService _nav = NavidromeService();
|
late final NavidromeService _nav = NavidromeService();
|
||||||
|
// Die Alben sind beim Prüfen schon geladen — kein zweiter Serveraufruf.
|
||||||
|
late final ServerNeuheiten _neuheiten =
|
||||||
|
ServerNeuheiten(holeAlben: () async => _albums);
|
||||||
List<SubsonicAlbum> _albums = [];
|
List<SubsonicAlbum> _albums = [];
|
||||||
List<SubsonicArtist> _artists = [];
|
List<SubsonicArtist> _artists = [];
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
@@ -94,6 +99,9 @@ class _ServerBrowserState extends State<_ServerBrowser> {
|
|||||||
_albums = alben;
|
_albums = alben;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
|
// Erst jetzt prüfen — _albums ist gefüllt.
|
||||||
|
await _neuheiten.pruefe();
|
||||||
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -187,6 +195,24 @@ class _ServerBrowserState extends State<_ServerBrowser> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (_neuheiten.neue.isNotEmpty)
|
||||||
|
MaterialBanner(
|
||||||
|
backgroundColor: MeloTheme.surfaceHigh,
|
||||||
|
content: Text(
|
||||||
|
_neuheiten.neue.length == 1
|
||||||
|
? 'Neu auf dem Server: ${_neuheiten.neue.first.name}'
|
||||||
|
: '${_neuheiten.neue.length} neue Alben auf dem Server',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await _neuheiten.alsGesehenMarkieren();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
},
|
||||||
|
child: const Text('Verstanden'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import 'database.dart';
|
|||||||
|
|
||||||
const _uuid = Uuid();
|
const _uuid = Uuid();
|
||||||
|
|
||||||
|
/// Zählt die Musikdateien, die der MediaStore kennt. Grundlage für die
|
||||||
|
/// automatische Erkennung: weicht die Zahl von der Bibliothek ab, ist etwas
|
||||||
|
/// dazugekommen oder verschwunden.
|
||||||
|
Future<int> countAndroidMediaStoreSongs() async {
|
||||||
|
final all = await OnAudioQuery().querySongs(uriType: UriType.EXTERNAL);
|
||||||
|
return all.where((s) => s.isMusic ?? true).length;
|
||||||
|
}
|
||||||
|
|
||||||
/// Android-Scan über MediaStore (der einzige zuverlässige Weg ab Android 11,
|
/// Android-Scan über MediaStore (der einzige zuverlässige Weg ab Android 11,
|
||||||
/// da Scoped Storage direkten Dateizugriff blockiert). Findet automatisch alle
|
/// da Scoped Storage direkten Dateizugriff blockiert). Findet automatisch alle
|
||||||
/// Musik auf dem Gerät. Schreibt in dieselbe DB wie der Desktop-Scan,
|
/// Musik auf dem Gerät. Schreibt in dieselbe DB wie der Desktop-Scan,
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// Erkennt von allein, wenn sich der Musikbestand des Geräts geändert hat —
|
||||||
|
/// etwa weil Dateien per Kabel übertragen oder heruntergeladen wurden — und
|
||||||
|
/// stößt dann einen Scan an.
|
||||||
|
///
|
||||||
|
/// Bewusst nur ein Zählvergleich: der Scan selbst ist der teure Teil und soll
|
||||||
|
/// nicht bei jedem Wechsel in den Vordergrund laufen. Werden gleich viele
|
||||||
|
/// Titel gelöscht wie hinzugefügt, merkt das erst der nächste Scan von Hand.
|
||||||
|
class AutoScan {
|
||||||
|
AutoScan({
|
||||||
|
required this.geraeteAnzahl,
|
||||||
|
required this.bekannteAnzahl,
|
||||||
|
required this.scanne,
|
||||||
|
this.mindestAbstand = const Duration(minutes: 5),
|
||||||
|
DateTime Function()? jetzt,
|
||||||
|
}) : _jetzt = jetzt ?? DateTime.now;
|
||||||
|
|
||||||
|
/// Wie viele Musikdateien das Gerät meldet.
|
||||||
|
final Future<int> Function() geraeteAnzahl;
|
||||||
|
|
||||||
|
/// Wie viele Titel die Bibliothek kennt.
|
||||||
|
final Future<int> Function() bekannteAnzahl;
|
||||||
|
|
||||||
|
final Future<void> Function() scanne;
|
||||||
|
|
||||||
|
/// So lange wird nach einer Prüfung gar nicht erst wieder gezählt.
|
||||||
|
final Duration mindestAbstand;
|
||||||
|
|
||||||
|
final DateTime Function() _jetzt;
|
||||||
|
|
||||||
|
DateTime? _zuletzt;
|
||||||
|
bool _laeuft = false;
|
||||||
|
|
||||||
|
/// Prüft, ob sich etwas geändert hat, und scannt gegebenenfalls.
|
||||||
|
/// Gibt zurück, ob ein Scan angestoßen wurde.
|
||||||
|
Future<bool> pruefe() async {
|
||||||
|
if (_laeuft) return false;
|
||||||
|
final jetzt = _jetzt();
|
||||||
|
final zuletzt = _zuletzt;
|
||||||
|
if (zuletzt != null && jetzt.difference(zuletzt) < mindestAbstand) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_zuletzt = jetzt;
|
||||||
|
|
||||||
|
_laeuft = true;
|
||||||
|
try {
|
||||||
|
final auf = await geraeteAnzahl();
|
||||||
|
final bekannt = await bekannteAnzahl();
|
||||||
|
if (auf == bekannt) return false;
|
||||||
|
await scanne();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
// Kein Grund, die App zu stören — der Knopf zum Scannen bleibt ja da.
|
||||||
|
debugPrint('Automatische Erkennung fehlgeschlagen: $e');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
_laeuft = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,6 +188,16 @@ class MeloDb extends _$MeloDb {
|
|||||||
/// Alle Songs inkl. getombstonte. Intern für ID-Stabilität beim Scan.
|
/// Alle Songs inkl. getombstonte. Intern für ID-Stabilität beim Scan.
|
||||||
Future<List<Song>> allSongs() => select(songs).get();
|
Future<List<Song>> allSongs() => select(songs).get();
|
||||||
|
|
||||||
|
/// Wie viele Titel die Bibliothek kennt (ohne getombstonte).
|
||||||
|
Future<int> countSongs() async {
|
||||||
|
final anzahl = songs.id.count();
|
||||||
|
final zeile = await (selectOnly(songs)
|
||||||
|
..addColumns([anzahl])
|
||||||
|
..where(songs.deleted.equals(false)))
|
||||||
|
.getSingle();
|
||||||
|
return zeile.read(anzahl) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Upsert Songs nach Pfad. Neue Songs bekommen UUID; bekannte erhalten sie zurück.
|
/// Upsert Songs nach Pfad. Neue Songs bekommen UUID; bekannte erhalten sie zurück.
|
||||||
/// Setzt [updatedAtMs] auf die Werte der Companions (normalerweise Scan-Startzeit).
|
/// Setzt [updatedAtMs] auf die Werte der Companions (normalerweise Scan-Startzeit).
|
||||||
Future<void> upsertSongs(List<SongsCompanion> items) async {
|
Future<void> upsertSongs(List<SongsCompanion> items) async {
|
||||||
|
|||||||
+33
-1
@@ -1,8 +1,12 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'downloads/downloads_screen.dart';
|
import 'downloads/downloads_screen.dart';
|
||||||
|
import 'library/android_scan.dart';
|
||||||
|
import 'library/auto_scan.dart';
|
||||||
import 'library/category_service.dart';
|
import 'library/category_service.dart';
|
||||||
import 'library/database.dart';
|
import 'library/database.dart';
|
||||||
import 'library/favorites_screen.dart';
|
import 'library/favorites_screen.dart';
|
||||||
@@ -107,11 +111,39 @@ class HomeShell extends StatefulWidget {
|
|||||||
State<HomeShell> createState() => _HomeShellState();
|
State<HomeShell> createState() => _HomeShellState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeShellState extends State<HomeShell> {
|
class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
|
late final AutoScan _autoScan;
|
||||||
|
|
||||||
void _goTo(int index) => setState(() => _index = index);
|
void _goTo(int index) => setState(() => _index = index);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final db = context.read<MeloDb>();
|
||||||
|
final lib = context.read<LibraryService>();
|
||||||
|
_autoScan = AutoScan(
|
||||||
|
geraeteAnzahl: countAndroidMediaStoreSongs,
|
||||||
|
bekannteAnzahl: db.countSongs,
|
||||||
|
scanne: lib.rescan,
|
||||||
|
);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
// Zurück in der App: nachsehen, ob inzwischen Musik dazugekommen ist.
|
||||||
|
if (state == AppLifecycleState.resumed && Platform.isAndroid) {
|
||||||
|
_autoScan.pruefe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tabs = <Widget>[
|
final tabs = <Widget>[
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'navidrome_service.dart';
|
||||||
|
|
||||||
|
/// Merkt sich, welche Alben auf dem Musikserver schon bekannt waren, und
|
||||||
|
/// meldet, was seitdem dazugekommen ist.
|
||||||
|
class ServerNeuheiten extends ChangeNotifier {
|
||||||
|
ServerNeuheiten({required this.holeAlben});
|
||||||
|
|
||||||
|
static const _key = 'server_bekannte_alben';
|
||||||
|
|
||||||
|
/// Holt die neuesten Alben vom Server.
|
||||||
|
final Future<List<SubsonicAlbum>> Function() holeAlben;
|
||||||
|
|
||||||
|
List<SubsonicAlbum> _neue = const [];
|
||||||
|
|
||||||
|
/// Alben, die seit dem letzten Nachsehen dazugekommen sind.
|
||||||
|
List<SubsonicAlbum> get neue => _neue;
|
||||||
|
|
||||||
|
Future<void> pruefe() async {
|
||||||
|
final List<SubsonicAlbum> alben;
|
||||||
|
try {
|
||||||
|
alben = await holeAlben();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Server-Neuheiten konnten nicht geladen werden: $e');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final bekannt = prefs.getStringList(_key);
|
||||||
|
|
||||||
|
// Beim allerersten Mal ist noch nichts bekannt. Alles als neu zu melden
|
||||||
|
// wäre unbrauchbar — also erst einmal nur den Stand festhalten.
|
||||||
|
if (bekannt == null) {
|
||||||
|
await prefs.setStringList(_key, alben.map((a) => a.id).toList());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final gesehen = bekannt.toSet();
|
||||||
|
_neue = alben.where((a) => !gesehen.contains(a.id)).toList();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nimmt die gemeldeten Alben zur Kenntnis — sie tauchen nicht wieder auf.
|
||||||
|
Future<void> alsGesehenMarkieren() async {
|
||||||
|
if (_neue.isEmpty) return;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final bekannt = (prefs.getStringList(_key) ?? const <String>[]).toSet()
|
||||||
|
..addAll(_neue.map((a) => a.id));
|
||||||
|
await prefs.setStringList(_key, bekannt.toList());
|
||||||
|
_neue = const [];
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:melo/library/auto_scan.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Mehr Dateien auf dem Gerät als in der Bibliothek löst einen Scan aus',
|
||||||
|
() async {
|
||||||
|
var gescannt = 0;
|
||||||
|
final auto = AutoScan(
|
||||||
|
geraeteAnzahl: () async => 12,
|
||||||
|
bekannteAnzahl: () async => 10,
|
||||||
|
scanne: () async => gescannt++,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await auto.pruefe(), isTrue);
|
||||||
|
expect(gescannt, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gleich viele Dateien lassen die Bibliothek in Ruhe', () async {
|
||||||
|
var gescannt = 0;
|
||||||
|
final auto = AutoScan(
|
||||||
|
geraeteAnzahl: () async => 10,
|
||||||
|
bekannteAnzahl: () async => 10,
|
||||||
|
scanne: () async => gescannt++,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await auto.pruefe(), isFalse);
|
||||||
|
expect(gescannt, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Auch gelöschte Dateien (weniger auf dem Gerät) lösen einen Scan aus',
|
||||||
|
() async {
|
||||||
|
var gescannt = 0;
|
||||||
|
final auto = AutoScan(
|
||||||
|
geraeteAnzahl: () async => 8,
|
||||||
|
bekannteAnzahl: () async => 10,
|
||||||
|
scanne: () async => gescannt++,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await auto.pruefe(), isTrue);
|
||||||
|
expect(gescannt, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Zwei Prüfungen kurz hintereinander zählen nur einmal', () async {
|
||||||
|
var gezaehlt = 0;
|
||||||
|
var jetzt = DateTime(2026, 8, 20, 12, 0);
|
||||||
|
final auto = AutoScan(
|
||||||
|
geraeteAnzahl: () async {
|
||||||
|
gezaehlt++;
|
||||||
|
return 12;
|
||||||
|
},
|
||||||
|
bekannteAnzahl: () async => 10,
|
||||||
|
scanne: () async {},
|
||||||
|
mindestAbstand: const Duration(minutes: 5),
|
||||||
|
jetzt: () => jetzt,
|
||||||
|
);
|
||||||
|
|
||||||
|
await auto.pruefe();
|
||||||
|
jetzt = jetzt.add(const Duration(minutes: 1));
|
||||||
|
expect(await auto.pruefe(), isFalse);
|
||||||
|
expect(gezaehlt, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Nach Ablauf des Mindestabstands wird wieder geprüft', () async {
|
||||||
|
var gezaehlt = 0;
|
||||||
|
var jetzt = DateTime(2026, 8, 20, 12, 0);
|
||||||
|
final auto = AutoScan(
|
||||||
|
geraeteAnzahl: () async {
|
||||||
|
gezaehlt++;
|
||||||
|
return 12;
|
||||||
|
},
|
||||||
|
bekannteAnzahl: () async => 10,
|
||||||
|
scanne: () async {},
|
||||||
|
mindestAbstand: const Duration(minutes: 5),
|
||||||
|
jetzt: () => jetzt,
|
||||||
|
);
|
||||||
|
|
||||||
|
await auto.pruefe();
|
||||||
|
jetzt = jetzt.add(const Duration(minutes: 6));
|
||||||
|
expect(await auto.pruefe(), isTrue);
|
||||||
|
expect(gezaehlt, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ein Fehler beim Zählen bleibt folgenlos', () async {
|
||||||
|
var gescannt = 0;
|
||||||
|
final auto = AutoScan(
|
||||||
|
geraeteAnzahl: () async => throw Exception('MediaStore streikt'),
|
||||||
|
bekannteAnzahl: () async => 10,
|
||||||
|
scanne: () async => gescannt++,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await auto.pruefe(), isFalse);
|
||||||
|
expect(gescannt, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:melo/library/database.dart';
|
||||||
|
|
||||||
|
Future<void> _insert(MeloDb db, String id, {bool deleted = false}) {
|
||||||
|
return db.into(db.songs).insert(SongsCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
path: '/$id.mp3',
|
||||||
|
title: id,
|
||||||
|
dateAddedMs: 0,
|
||||||
|
updatedAtMs: 0,
|
||||||
|
deleted: Value(deleted),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('countSongs zählt nur die Titel, die noch da sind', () async {
|
||||||
|
final db = MeloDb(NativeDatabase.memory());
|
||||||
|
await _insert(db, 'a');
|
||||||
|
await _insert(db, 'b');
|
||||||
|
await _insert(db, 'c', deleted: true);
|
||||||
|
|
||||||
|
expect(await db.countSongs(), 2);
|
||||||
|
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:melo/services/navidrome_service.dart';
|
||||||
|
import 'package:melo/services/server_neuheiten.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
SubsonicAlbum _album(String id) => SubsonicAlbum(id: id, name: 'Album $id');
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||||
|
|
||||||
|
test('Der erste Abruf meldet nichts Neues — sonst wäre alles neu', () async {
|
||||||
|
final n = ServerNeuheiten(
|
||||||
|
holeAlben: () async => [_album('1'), _album('2')]);
|
||||||
|
|
||||||
|
await n.pruefe();
|
||||||
|
|
||||||
|
expect(n.neue, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ein danach hinzugekommenes Album wird gemeldet', () async {
|
||||||
|
var alben = [_album('1'), _album('2')];
|
||||||
|
final n = ServerNeuheiten(holeAlben: () async => alben);
|
||||||
|
await n.pruefe();
|
||||||
|
|
||||||
|
alben = [_album('3'), _album('1'), _album('2')];
|
||||||
|
await n.pruefe();
|
||||||
|
|
||||||
|
expect(n.neue.map((a) => a.id), ['3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ohne Änderung bleibt die Liste leer', () async {
|
||||||
|
final n = ServerNeuheiten(
|
||||||
|
holeAlben: () async => [_album('1'), _album('2')]);
|
||||||
|
await n.pruefe();
|
||||||
|
await n.pruefe();
|
||||||
|
|
||||||
|
expect(n.neue, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Als gesehen markieren räumt die Liste weg und wiederholt sich nicht',
|
||||||
|
() async {
|
||||||
|
var alben = [_album('1')];
|
||||||
|
final n = ServerNeuheiten(holeAlben: () async => alben);
|
||||||
|
await n.pruefe();
|
||||||
|
alben = [_album('1'), _album('2')];
|
||||||
|
await n.pruefe();
|
||||||
|
expect(n.neue, isNotEmpty);
|
||||||
|
|
||||||
|
await n.alsGesehenMarkieren();
|
||||||
|
expect(n.neue, isEmpty);
|
||||||
|
|
||||||
|
await n.pruefe();
|
||||||
|
expect(n.neue, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ein Serverfehler bleibt folgenlos', () async {
|
||||||
|
final n = ServerNeuheiten(
|
||||||
|
holeAlben: () async => throw Exception('Server weg'));
|
||||||
|
|
||||||
|
await n.pruefe();
|
||||||
|
|
||||||
|
expect(n.neue, isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user