Fix: Server-Bibliothek lädt nicht + Tab '🌐 Server' abgeschnitten
Bug 2 (Laden schlägt fehl trotz 'ping ok'): Der Code ignorierte das Subsonic-
status-Feld. Navidrome liefert bei falschem Passwort HTTP 200 mit
status:"failed" — ping() meldete dadurch fälschlich Erfolg, getAlben/getArtists
behandelten die Fehlerantwort still als leer ("Erneut versuchen").
- parseSubsonic() wirft NavidromeException bei status:"failed" (mit Servermsg).
- ping() prüft jetzt den Body statt nur HTTP 200.
- getAlben/getArtists reichen den Fehler durch (rethrow) statt [] zu liefern.
- getAlben nutzt getAlbumList2 (ID3, wie getArtists) statt getAlbumList (Folder).
- Server-Tab zeigt die echte Fehlermeldung statt "Keine Alben geladen".
Bug 1 (Tab abgeschnitten -> nur "Serve"): TabBar labelPadding verkleinert
(engere Abstände), Schriftgröße unverändert.
Neue Tests: test/navidrome_parse_test.dart (5). 84/84 grün, analyze sauber.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr2UwurtxyJUpWpBSeVmBm
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
61e28d128f
commit
3d39e8b922
@@ -58,6 +58,16 @@ class SubsonicAlbum {
|
||||
);
|
||||
}
|
||||
|
||||
/// Fehler einer Subsonic-Antwort mit `status:"failed"` (Navidrome liefert
|
||||
/// dafür HTTP 200). Trägt die Server-Meldung, damit die UI sie zeigen kann.
|
||||
class NavidromeException implements Exception {
|
||||
final String message;
|
||||
final int? code;
|
||||
NavidromeException(this.message, {this.code});
|
||||
@override
|
||||
String toString() => 'NavidromeException(${code ?? '-'}): $message';
|
||||
}
|
||||
|
||||
class NavidromeService {
|
||||
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
||||
|
||||
@@ -123,6 +133,35 @@ class NavidromeService {
|
||||
|
||||
bool get istVerbunden => _serverUrl.isNotEmpty && _user.isNotEmpty;
|
||||
|
||||
/// Dekodiert eine Subsonic-JSON-Antwort und wirft [NavidromeException],
|
||||
/// wenn der Server `status:"failed"` meldet (z. B. falsches Passwort) —
|
||||
/// solche Antworten kommen mit HTTP 200 und dürfen nicht als „leer" gelten.
|
||||
@visibleForTesting
|
||||
static Map<String, dynamic> parseSubsonic(String body) {
|
||||
final data = jsonDecode(body) as Map<String, dynamic>;
|
||||
final resp = data['subsonic-response'] as Map<String, dynamic>?;
|
||||
if (resp == null) {
|
||||
throw NavidromeException('Ungültige Server-Antwort (kein subsonic-response)');
|
||||
}
|
||||
if (resp['status'] == 'failed') {
|
||||
final err = resp['error'] as Map<String, dynamic>?;
|
||||
throw NavidromeException(
|
||||
(err?['message'] as String?) ?? 'Unbekannter Server-Fehler',
|
||||
code: err?['code'] as int?,
|
||||
);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
/// Liest Alben aus einer geprüften `getAlbumList2`-Antwort (ID3-basiert).
|
||||
@visibleForTesting
|
||||
static List<SubsonicAlbum> parseAlben(Map<String, dynamic> resp) {
|
||||
final list = resp['albumList2']?['album'] as List? ?? [];
|
||||
return list
|
||||
.map((j) => SubsonicAlbum.fromJson(j as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Uri _uri(String endpoint, [Map<String, String>? extra]) {
|
||||
if (_token == null || _token!.isEmpty) {
|
||||
throw StateError('Navidrome ist nicht verbunden. Bitte zuerst Zugangsdaten setzen.');
|
||||
@@ -142,13 +181,18 @@ class NavidromeService {
|
||||
Future<bool> ping() async {
|
||||
try {
|
||||
final r = await http.get(_uri('ping.view')).timeout(const Duration(seconds: 10));
|
||||
if (r.statusCode == 200) {
|
||||
logger.info('✅ Navidrome Verbindung erfolgreich: $_serverUrl');
|
||||
return true;
|
||||
} else {
|
||||
if (r.statusCode != 200) {
|
||||
logger.warning('Navidrome Ping fehlgeschlagen (HTTP ${r.statusCode}): $_serverUrl');
|
||||
return false;
|
||||
}
|
||||
// Subsonic liefert auch bei falschem Passwort HTTP 200 → Body prüfen,
|
||||
// sonst meldet der Login fälschlich Erfolg (status:"failed").
|
||||
parseSubsonic(r.body);
|
||||
logger.info('✅ Navidrome Verbindung erfolgreich: $_serverUrl');
|
||||
return true;
|
||||
} on NavidromeException catch (e) {
|
||||
logger.warning('Navidrome Login abgelehnt: ${e.message}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
// Details für die Diagnose: ohne URL/Exception-Typ ist im Log nicht
|
||||
// erkennbar, ob DNS, IPv6, TLS oder eine falsche URL die Ursache ist.
|
||||
@@ -181,14 +225,16 @@ class NavidromeService {
|
||||
|
||||
Future<List<SubsonicAlbum>> getAlben({int anzahl = 50}) async {
|
||||
try {
|
||||
final r = await http.get(_uri('getAlbumList.view', {'type': 'newest', 'size': '$anzahl'})).timeout(const Duration(seconds: 15));
|
||||
if (r.statusCode != 200) return [];
|
||||
final data = jsonDecode(r.body);
|
||||
final list = data['subsonic-response']?['albumList']?['album'] as List? ?? [];
|
||||
return list.map((j) => SubsonicAlbum.fromJson(j)).toList();
|
||||
// getAlbumList2 = ID3-basiert (wie getArtists) — der Folder-Endpoint
|
||||
// getAlbumList liefert bei tag-organisierten Navidrome-Libraries leer.
|
||||
final r = await http
|
||||
.get(_uri('getAlbumList2.view', {'type': 'newest', 'size': '$anzahl'}))
|
||||
.timeout(const Duration(seconds: 15));
|
||||
if (r.statusCode != 200) throw NavidromeException('HTTP ${r.statusCode}');
|
||||
return parseAlben(parseSubsonic(r.body));
|
||||
} catch (e) {
|
||||
await logger.error('Navidrome getAlben Fehler: $e', e, StackTrace.current);
|
||||
return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,20 +295,20 @@ class NavidromeService {
|
||||
Future<List<SubsonicArtist>> getArtists() async {
|
||||
try {
|
||||
final r = await http.get(_uri('getArtists.view')).timeout(const Duration(seconds: 15));
|
||||
if (r.statusCode != 200) return [];
|
||||
final data = jsonDecode(r.body);
|
||||
final indexData = data['subsonic-response']?['artists']?['index'] as List? ?? [];
|
||||
if (r.statusCode != 200) throw NavidromeException('HTTP ${r.statusCode}');
|
||||
final resp = parseSubsonic(r.body);
|
||||
final indexData = resp['artists']?['index'] as List? ?? [];
|
||||
final artists = <SubsonicArtist>[];
|
||||
for (final idx in indexData) {
|
||||
final artistList = idx['artist'] as List? ?? [];
|
||||
for (final a in artistList) {
|
||||
artists.add(SubsonicArtist.fromJson(a));
|
||||
artists.add(SubsonicArtist.fromJson(a as Map<String, dynamic>));
|
||||
}
|
||||
}
|
||||
return artists;
|
||||
} catch (e) {
|
||||
await logger.error('Navidrome getArtists Fehler: $e', e, StackTrace.current);
|
||||
return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user