Herz-Tipp meldet den Favoriten sofort deterministisch an die Cloud
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../services/melo_cloud_service.dart';
|
||||
import '../services/navidrome_service.dart';
|
||||
import 'database.dart';
|
||||
|
||||
@@ -6,11 +7,18 @@ import 'database.dart';
|
||||
/// nach jeder Mutation (für Feedback wie SnackBars — die Listen selbst
|
||||
/// beobachten UIs direkt über die watch()-Streams von [MeloDb]).
|
||||
class PlaylistService extends ChangeNotifier {
|
||||
PlaylistService(this.db, {NavidromeService? navidrome})
|
||||
: _navidrome = navidrome ?? NavidromeService();
|
||||
// `this._cloud` würde den öffentlichen Parameternamen `cloud` zu `_cloud`
|
||||
// (privat) ändern und die API brechen.
|
||||
PlaylistService(this.db, {NavidromeService? navidrome, MeloCloudService? cloud})
|
||||
: _navidrome = navidrome ?? NavidromeService(),
|
||||
// ignore: prefer_initializing_formals
|
||||
_cloud = cloud;
|
||||
final MeloDb db;
|
||||
final NavidromeService _navidrome;
|
||||
|
||||
/// Optional: ohne Cloud-Zugang entfällt der Sofort-Push stillschweigend.
|
||||
final MeloCloudService? _cloud;
|
||||
|
||||
Future<String> createPlaylist(String name, {String? description}) async {
|
||||
final id = await db.createPlaylist(name, description: description);
|
||||
notifyListeners();
|
||||
@@ -45,6 +53,26 @@ class PlaylistService extends ChangeNotifier {
|
||||
Future<void> toggleFavorite(String songId) async {
|
||||
await db.toggleFavorite(songId);
|
||||
notifyListeners();
|
||||
await _meldeFavorit(songId);
|
||||
}
|
||||
|
||||
/// Meldet den neuen Favoriten-Stand sofort an die Melo-Cloud.
|
||||
///
|
||||
/// Deterministisch (`set:true/false`), nicht als Umschalten: ein
|
||||
/// abweichender Server-Stand darf den Wunsch nicht invertieren. Nur für
|
||||
/// Titel mit cloudId, und Fehler werden still geschluckt — der nächste
|
||||
/// Abgleich pusht additiv nach.
|
||||
Future<void> _meldeFavorit(String songId) async {
|
||||
final cloud = _cloud;
|
||||
if (cloud == null || !cloud.istAngemeldet) return;
|
||||
final cloudId = (await db.songById(songId))?.cloudId;
|
||||
if (cloudId == null) return;
|
||||
final gesetzt = await db.watchIsFavorite(songId).first;
|
||||
try {
|
||||
await cloud.setzeFavorit(cloudId, gesetzt);
|
||||
} catch (e) {
|
||||
debugPrint('Favorit nicht gemeldet: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Lädt alle Favoriten vom Navidrome-Server und importiert sie lokal.
|
||||
|
||||
+6
-1
@@ -51,7 +51,6 @@ Future<void> main() async {
|
||||
await logger.init();
|
||||
_db = MeloDb();
|
||||
_library = LibraryService(_db);
|
||||
_playlists = PlaylistService(_db);
|
||||
_offlineMode = OfflineMode();
|
||||
await _offlineMode.init();
|
||||
_categories = CategoryService(_db);
|
||||
@@ -83,6 +82,12 @@ Future<void> main() async {
|
||||
cloud: MeloCloudService(auth: _bakaAuth),
|
||||
);
|
||||
await _sync.laden();
|
||||
// Erst hier: der Sofort-Push braucht den angemeldeten Cloud-Zugang, und
|
||||
// _bakaAuth entsteht weiter oben.
|
||||
_playlists = PlaylistService(
|
||||
_db,
|
||||
cloud: MeloCloudService(auth: _bakaAuth),
|
||||
);
|
||||
final navidrome = NavidromeService();
|
||||
await navidrome.ladeGespeicherteZugangsdaten();
|
||||
_downloads = DownloadService(db: _db, navidrome: navidrome);
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:melo/library/database.dart';
|
||||
import 'package:melo/library/playlist_service.dart';
|
||||
import 'package:melo/services/baka_auth.dart';
|
||||
import 'package:melo/services/melo_cloud_service.dart';
|
||||
|
||||
class _MemorySpeicher implements TokenSpeicher {
|
||||
_MemorySpeicher(this.werte);
|
||||
final Map<String, String> werte;
|
||||
@override
|
||||
Future<String?> lesen(String key) async => werte[key];
|
||||
@override
|
||||
Future<void> schreiben(String key, String wert) async => werte[key] = wert;
|
||||
@override
|
||||
Future<void> loeschen(String key) async => werte.remove(key);
|
||||
}
|
||||
|
||||
Future<BakaAuth> _angemeldeteAuth() async {
|
||||
final auth = BakaAuth(
|
||||
speicher: _MemorySpeicher({'baka_token': 'tok', 'baka_user': 'Baka'}),
|
||||
);
|
||||
await auth.laden();
|
||||
return auth;
|
||||
}
|
||||
|
||||
void main() {
|
||||
late MeloDb db;
|
||||
@@ -61,4 +86,110 @@ void main() {
|
||||
expect(songs.map((s) => s.id).toList(), ['song-3', 'song-1', 'song-2']);
|
||||
expect(notified, true);
|
||||
});
|
||||
|
||||
group('Sofort-Push der Favoriten', () {
|
||||
Future<void> legeSongAn(MeloDb db, {String? cloudId}) async {
|
||||
await db.into(db.songs).insert(SongsCompanion.insert(
|
||||
id: 'song-1',
|
||||
path: '/a.mp3',
|
||||
title: 'A',
|
||||
dateAddedMs: 0,
|
||||
updatedAtMs: 0,
|
||||
));
|
||||
if (cloudId != null) await db.setCloudId('song-1', cloudId);
|
||||
}
|
||||
|
||||
test('setzt deterministisch true beim Favorisieren', () async {
|
||||
final db2 = MeloDb(NativeDatabase.memory());
|
||||
addTearDown(db2.close);
|
||||
await legeSongAn(db2, cloudId: 'c1');
|
||||
|
||||
final gesendet = <Map<String, dynamic>>[];
|
||||
final dienst = PlaylistService(
|
||||
db2,
|
||||
cloud: MeloCloudService(
|
||||
auth: await _angemeldeteAuth(),
|
||||
client: MockClient((anfrage) async {
|
||||
gesendet.add(jsonDecode(anfrage.body) as Map<String, dynamic>);
|
||||
return http.Response(jsonEncode({'status': 'ok'}), 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await dienst.toggleFavorite('song-1');
|
||||
|
||||
expect(gesendet, [
|
||||
{'song_id': 'c1', 'set': true}
|
||||
]);
|
||||
});
|
||||
|
||||
test('setzt deterministisch false beim Ent-Favorisieren', () async {
|
||||
final db2 = MeloDb(NativeDatabase.memory());
|
||||
addTearDown(db2.close);
|
||||
await legeSongAn(db2, cloudId: 'c1');
|
||||
await db2.setFavorite('song-1', true);
|
||||
|
||||
final gesendet = <Map<String, dynamic>>[];
|
||||
final dienst = PlaylistService(
|
||||
db2,
|
||||
cloud: MeloCloudService(
|
||||
auth: await _angemeldeteAuth(),
|
||||
client: MockClient((anfrage) async {
|
||||
gesendet.add(jsonDecode(anfrage.body) as Map<String, dynamic>);
|
||||
return http.Response(jsonEncode({'status': 'ok'}), 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await dienst.toggleFavorite('song-1');
|
||||
|
||||
expect(gesendet, [
|
||||
{'song_id': 'c1', 'set': false}
|
||||
]);
|
||||
});
|
||||
|
||||
test('ohne cloudId wird nichts gemeldet', () async {
|
||||
final db2 = MeloDb(NativeDatabase.memory());
|
||||
addTearDown(db2.close);
|
||||
await legeSongAn(db2);
|
||||
|
||||
var anfragen = 0;
|
||||
final dienst = PlaylistService(
|
||||
db2,
|
||||
cloud: MeloCloudService(
|
||||
auth: await _angemeldeteAuth(),
|
||||
client: MockClient((_) async {
|
||||
anfragen++;
|
||||
return http.Response(jsonEncode({'status': 'ok'}), 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await dienst.toggleFavorite('song-1');
|
||||
|
||||
expect(anfragen, 0);
|
||||
expect(await db2.watchIsFavorite('song-1').first, isTrue);
|
||||
});
|
||||
|
||||
test('ein Fehler des Servers ändert lokal nichts und wirft nicht',
|
||||
() async {
|
||||
final db2 = MeloDb(NativeDatabase.memory());
|
||||
addTearDown(db2.close);
|
||||
await legeSongAn(db2, cloudId: 'c1');
|
||||
|
||||
final dienst = PlaylistService(
|
||||
db2,
|
||||
cloud: MeloCloudService(
|
||||
auth: await _angemeldeteAuth(),
|
||||
client: MockClient(
|
||||
(_) async => http.Response(jsonEncode({'error': 'weg'}), 500)),
|
||||
),
|
||||
);
|
||||
|
||||
// Der nächste Voll-Abgleich holt den Push additiv nach.
|
||||
await dienst.toggleFavorite('song-1');
|
||||
|
||||
expect(await db2.watchIsFavorite('song-1').first, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user