Files
Melo/test/library/song_sort_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

140 lines
4.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter_test/flutter_test.dart';
import 'package:melo/library/database.dart';
import 'package:melo/library/song_sort.dart';
Song _song(
String title, {
String? id,
int dateAddedMs = 0,
int playCount = 0,
}) {
return Song(
id: id ?? title,
path: '/$title.mp3',
title: title,
dateAddedMs: dateAddedMs,
updatedAtMs: 0,
deleted: false,
playCount: playCount,
categoriesEdited: false,
metadataEdited: false,
);
}
void main() {
group('SortMode.name — A-Z, Ziffern/Zeichen unter # ans Ende', () {
test('sortiert Buchstaben alphabetisch, Ziffern und Zeichen danach', () {
final songs = [
_song('9 Lives'),
_song('Zenit'),
_song('#Hashtag'),
_song('Anfang'),
];
final sorted = sortSongs(songs, SortMode.name, ascending: true);
expect(sorted.map((s) => s.title), ['Anfang', 'Zenit', '#Hashtag', '9 Lives']);
});
test('ignoriert Groß-/Kleinschreibung', () {
final songs = [_song('beta'), _song('Alpha'), _song('CHARLIE')];
final sorted = sortSongs(songs, SortMode.name, ascending: true);
expect(sorted.map((s) => s.title), ['Alpha', 'beta', 'CHARLIE']);
});
test('Umlaute sortieren wie ihr Grundbuchstabe', () {
final songs = [_song('Zug'), _song('Ärger'), _song('Anfang'), _song('Öl')];
final sorted = sortSongs(songs, SortMode.name, ascending: true);
expect(sorted.map((s) => s.title), ['Anfang', 'Ärger', 'Öl', 'Zug']);
});
test('absteigend dreht die Reihenfolge um', () {
final songs = [_song('Anfang'), _song('9 Lives'), _song('Zenit')];
final sorted = sortSongs(songs, SortMode.name, ascending: false);
expect(sorted.map((s) => s.title), ['9 Lives', 'Zenit', 'Anfang']);
});
});
group('SortMode.dateAdded', () {
test('absteigend = zuletzt hinzugefügt zuerst', () {
final songs = [
_song('Alt', dateAddedMs: 100),
_song('Neu', dateAddedMs: 300),
_song('Mittel', dateAddedMs: 200),
];
final sorted = sortSongs(songs, SortMode.dateAdded, ascending: false);
expect(sorted.map((s) => s.title), ['Neu', 'Mittel', 'Alt']);
});
test('aufsteigend = ältester zuerst', () {
final songs = [
_song('Neu', dateAddedMs: 300),
_song('Alt', dateAddedMs: 100),
];
final sorted = sortSongs(songs, SortMode.dateAdded, ascending: true);
expect(sorted.map((s) => s.title), ['Alt', 'Neu']);
});
});
group('SortMode.playCount', () {
test('absteigend = meistgespielt zuerst', () {
final songs = [
_song('Selten', playCount: 1),
_song('Oft', playCount: 42),
_song('Nie', playCount: 0),
];
final sorted = sortSongs(songs, SortMode.playCount, ascending: false);
expect(sorted.map((s) => s.title), ['Oft', 'Selten', 'Nie']);
});
});
group('Stabilität', () {
test('gleiche Sortierwerte werden nach Titel entschieden', () {
final songs = [
_song('Bravo', id: 'b', dateAddedMs: 5),
_song('Alpha', id: 'a', dateAddedMs: 5),
];
final sorted = sortSongs(songs, SortMode.dateAdded, ascending: false);
expect(sorted.map((s) => s.title), ['Alpha', 'Bravo']);
});
test('verändert die Eingabeliste nicht', () {
final songs = [_song('Zenit'), _song('Anfang')];
sortSongs(songs, SortMode.name, ascending: true);
expect(songs.map((s) => s.title), ['Zenit', 'Anfang']);
});
});
group('Standard-Richtung pro Modus', () {
test('Datum und Wiedergaben starten absteigend, Name aufsteigend', () {
expect(defaultAscending(SortMode.dateAdded), isFalse);
expect(defaultAscending(SortMode.playCount), isFalse);
expect(defaultAscending(SortMode.name), isTrue);
});
});
group('Beschriftungen', () {
test('jeder Modus hat ein deutsches Label', () {
expect(SortMode.dateAdded.label, 'Zuletzt hinzugefügt');
expect(SortMode.name.label, 'Name (AZ#)');
expect(SortMode.playCount.label, 'Wie oft abgespielt');
});
});
}