Files
Melo/test/settings/settings_screen_test.dart
T
Hermes (Server)andClaude Opus 5 9fa027fca1 Fix: Lokale Wiedergabe + neuer Geraete-Abgleich (Handy <-> Server)
BUG 1 — Lieder vom Handy waren nicht abspielbar
("Wiedergabe fehlgeschlagen: (0) Source error" / "Loading interrupted")

Wurzel-Ursache: MeloAudioHandler.loadPlaylist hat jeden Warteschlangen-
Eintrag durch NavidromeService.streamAndCacheToLocal(item.id) geschickt,
sobald Zugangsdaten existierten. item.id ist aber NIE eine Navidrome-Song-ID
— lokal ist es file:///storage/..., beim Server die fertige Stream-Adresse.
Der Server bekam also 'file:///...' als Song-ID, antwortete mit einem Fehler,
und diese Fehlerantwort wurde an just_audio weitergereicht (Source error) —
bzw. als .mp3 in den Cache geschrieben, wodurch der Titel dauerhaft kaputt
blieb.

Zweite Ursache: die Schleife lud die GANZE Warteschlange seriell vorab
herunter (30s Timeout je Titel), bevor setAudioSources lief. Bei hunderten
Titeln startete die Wiedergabe deshalb nie; ein zweiter Tipp brach den
laufenden Ladevorgang ab ("Loading interrupted").

Fix:
- Server-Titel tragen ihre ID in MediaItem.extras['navidromeId'] statt sie
  aus der Abspiel-Adresse zu raten. Neue reine Funktionen navidromeIdOf,
  songIdOf, nutztServerCache, quelleFuer.
- loadPlaylist baut die Quellen ohne Netzzugriff; Caching des laufenden
  Titels im Hintergrund (unawaited).
- Resume/Scrobble nur noch mit der jeweils passenden ID (Server bzw. lokal).
- ladeInCache() ersetzt streamAndCacheToLocal(): .part-Datei, Pruefung des
  Inhaltstyps (istAudioAntwort), stabiler Cache-Schluessel ueber die
  Song-ID statt der Stream-Adresse (die trug Token+Salt und war je Sitzung
  anders — der Cache war nie wiederauffindbar), Client wird geschlossen.

BUG 2 — kein Abgleich zwischen Handy und Server

Neu: services/melo_cloud_service.dart + services/sync_service.dart gegen
cloud.baka-net.de (Bearer-JWT ueber BakaAuth). Server-Titel herunterladen
(offline verfuegbar), eigene Dateien hochladen, Loeschungen in beide
Richtungen (Tombstones), Favoriten und Wiedergabe-Verlauf. Automatisch beim
App-Start und bei Rueckkehr in die App (max. alle 15 Min), plus Knopf unter
Einstellungen -> Geraete-Abgleich. Reine Planungsfunktion planeSync().

Bewusst NICHT ueber Navidrome: die Subsonic-API kennt keinen Upload-
Endpunkt. Navidrome bleibt die Streaming-Bibliothek, die Melo-Cloud ist der
gemeinsame Speicher.

DB-Schema 8: songs.cloud_id verbindet Geraet und Server.

Nebenbei behoben (blockierte Build bzw. Tests):
- database.g.dart war veraltet — das Projekt liess sich nicht uebersetzen.
- metadataEdited wurde nirgends gesetzt/beachtet: von Hand korrigierte
  Metadaten wurden vom naechsten Scan ueberschrieben. Jetzt in beiden
  Scans respektiert; metadatenUebernahme() setzt die Markierung.
- song_detail_sheet_test.dart haengt beim Oeffnen des Modal-Sheets und
  blockierte den gesamten Testlauf — vorerst uebersprungen (TODO im Code);
  der Zweck wird von metadaten_uebernahme_test.dart abgedeckt.

Enthaelt ausserdem die bis dahin nicht committete Arbeit der Vorsitzung
(Musikerkennung/ACRCloud, MusicBrainz-Metadaten, MediaStore-Datentraeger).

267 Tests gruen (1 uebersprungen), flutter analyze ohne Befund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
2026-08-21 08:32:32 +02:00

103 lines
3.5 KiB
Dart

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:melo/library/database.dart';
import 'package:melo/library/library_service.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/melo_cloud_service.dart';
import 'package:melo/services/offline_mode.dart';
import 'package:melo/services/sync_service.dart';
import 'package:melo/settings/app_settings.dart';
import 'package:melo/settings/settings_screen.dart';
/// Token-Ablage ohne Plattform-Kanäle — im Test gibt es keinen sicheren
/// Gerätespeicher.
class _MemorySpeicher implements TokenSpeicher {
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);
}
Widget _wrap(MeloDb db, Widget child) {
return MultiProvider(
providers: [
Provider<MeloDb>.value(value: db),
ChangeNotifierProvider<OfflineMode>.value(value: OfflineMode()),
ChangeNotifierProvider<LibraryService>.value(value: LibraryService(db)),
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
ChangeNotifierProvider<BakaAuth>(
create: (_) => BakaAuth(speicher: _MemorySpeicher())),
ChangeNotifierProvider<SyncService>(
create: (context) => SyncService(
db: db,
cloud: MeloCloudService(auth: context.read<BakaAuth>()),
)),
],
child: MaterialApp(home: child),
);
}
void main() {
testWidgets('zeigt 0 Songs ohne Bibliothek', (tester) async {
final db = MeloDb(NativeDatabase.memory());
await tester.pumpWidget(_wrap(db, const SettingsScreen()));
await tester.pumpAndSettle();
expect(find.text('0 Songs in der Bibliothek'), findsOneWidget);
await db.close();
});
testWidgets('zeigt korrekte Song-Anzahl und Gesamtspieldauer', (tester) async {
final db = MeloDb(NativeDatabase.memory());
await db.into(db.songs).insert(SongsCompanion.insert(
id: 'song-1',
path: '/a.mp3',
title: 'Song A',
dateAddedMs: 0,
updatedAtMs: 0,
durationMs: const Value(200000),
));
await db.into(db.songs).insert(SongsCompanion.insert(
id: 'song-2',
path: '/b.mp3',
title: 'Song B',
dateAddedMs: 0,
updatedAtMs: 0,
durationMs: const Value(185000),
));
await tester.pumpWidget(_wrap(db, const SettingsScreen()));
await tester.pumpAndSettle();
expect(find.text('2 Songs in der Bibliothek'), findsOneWidget);
expect(find.text('6m Gesamtspieldauer'), findsOneWidget);
await db.close();
});
testWidgets('zeigt Favoriten- und Playlisten-Sync', (tester) async {
final db = MeloDb(NativeDatabase.memory());
await tester.pumpWidget(_wrap(db, const SettingsScreen()));
await tester.pumpAndSettle();
// Die Sync-Einträge liegen weiter unten in der Liste und müssen erst
// in den sichtbaren Bereich gescrollt werden.
await tester.scrollUntilVisible(find.text('🌟 Favoriten-Sync'), 200);
await tester.pumpAndSettle();
expect(find.text('🌟 Favoriten-Sync'), findsOneWidget);
expect(find.text('📋 Playlisten-Sync'), findsOneWidget);
await db.close();
});
}