import 'dart:convert'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SubsonicSong { final String id; final String titel; final String kuenstler; final String album; final int dauerSekunden; final String? coverId; const SubsonicSong({ required this.id, required this.titel, this.kuenstler = '', this.album = '', this.dauerSekunden = 0, this.coverId, }); factory SubsonicSong.fromJson(Map j) => SubsonicSong( id: j['id'] as String, titel: j['title'] as String? ?? 'Unbekannt', kuenstler: j['artist'] as String? ?? '', album: j['album'] as String? ?? '', dauerSekunden: (j['duration'] as int?) ?? 0, coverId: j['coverArt'] as String?, ); } class SubsonicAlbum { final String id; final String name; final String? coverId; final int songCount; const SubsonicAlbum({ required this.id, required this.name, this.coverId, this.songCount = 0, }); factory SubsonicAlbum.fromJson(Map j) => SubsonicAlbum( id: j['id'] as String, name: j['name'] as String? ?? j['title'] as String? ?? 'Unbekannt', coverId: j['coverArt'] as String?, songCount: (j['songCount'] as int?) ?? 0, ); } class NavidromeService { final FlutterSecureStorage _secure = const FlutterSecureStorage(); String _serverUrl = ''; String _user = ''; String _password = ''; String _salt = ''; String? _token; static const _version = '1.16.1'; static const _client = 'Melo'; void setCredentials(String url, String user, String password) { _serverUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; _user = user; _password = password; final rng = Random.secure(); _salt = base64Encode(List.generate(16, (_) => rng.nextInt(256))); _token = md5.convert(utf8.encode(_password + _salt)).toString(); } Future ladeGespeicherteZugangsdaten() async { try { final prefs = await SharedPreferences.getInstance(); final url = prefs.getString('navidrome_url'); final user = prefs.getString('navidrome_user'); final pass = await _secure.read(key: 'navidrome_pass'); if (url != null && user != null && pass != null && url.isNotEmpty) { setCredentials(url, user, pass); } } catch (e) { debugPrint('Fehler beim Laden der Navidrome-Zugangsdaten: $e'); } } Future speichereZugangsdaten(String url, String user, String password) async { setCredentials(url, user, password); try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('navidrome_url', url); await prefs.setString('navidrome_user', user); await _secure.write(key: 'navidrome_pass', value: password); } catch (e) { debugPrint('Fehler beim Speichern der Navidrome-Zugangsdaten: $e'); } } Future loescheZugangsdaten() async { _serverUrl = ''; _user = ''; _password = ''; _salt = ''; _token = null; try { final prefs = await SharedPreferences.getInstance(); await prefs.remove('navidrome_url'); await prefs.remove('navidrome_user'); await _secure.delete(key: 'navidrome_pass'); } catch (e) { debugPrint('Fehler beim Löschen der Navidrome-Zugangsdaten: $e'); } } bool get istVerbunden => _serverUrl.isNotEmpty && _user.isNotEmpty; Uri _uri(String endpoint, [Map? extra]) { final params = { 'u': _user, 't': _token!, 's': _salt, 'v': _version, 'c': _client, 'f': 'json', }; if (extra != null) params.addAll(extra); return Uri.parse('$_serverUrl/rest/$endpoint').replace(queryParameters: params); } Future ping() async { try { final r = await http.get(_uri('ping.view')).timeout(const Duration(seconds: 10)); return r.statusCode == 200; } catch (e) { debugPrint('Navidrome Ping Fehler: $e'); return false; } } Future> 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(); } catch (e) { debugPrint('Navidrome getAlben Fehler: $e'); return []; } } Future> getSongs(String albumId) async { try { final r = await http.get(_uri('getAlbum.view', {'id': albumId})).timeout(const Duration(seconds: 15)); if (r.statusCode != 200) return []; final data = jsonDecode(r.body); final songs = data['subsonic-response']?['album']?['song'] as List? ?? []; return songs.map((j) => SubsonicSong.fromJson(j)).toList(); } catch (e) { debugPrint('Navidrome getSongs Fehler: $e'); return []; } } Uri streamUrl(String songId) { return _uri('stream.view', {'id': songId, 'format': 'raw'}); } Uri? coverUrl(String coverId) { return _uri('getCoverArt.view', {'id': coverId}); } }