import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import '../config/app_config.dart'; import '../services/auth_service.dart'; import '../services/melo_logger.dart'; /// Cloud-Sync Service für Melo Registry (v3 — vollständiges Sync-System) /// Authentifizierung via Baka-Auth JWT-Token class CloudService { static final http.Client _client = http.Client(); static String get _base => AppConfig.cloudUrl; /// Login mit Baka-Auth – Token wird aus AuthService bezogen Future login(String user) async { try { final r = await _client .get(Uri.parse('$_base/api/cloud/status'), headers: _authHeader) .timeout(const Duration(seconds: 5)); return r.statusCode == 200; } catch (_) { return false; } } Map get _authHeader { final token = AuthService().token; final headers = { 'X-API-Key': AppConfig.ytProxyApiKey, }; if (token != null && token.isNotEmpty) { headers['Authorization'] = 'Bearer $token'; } return headers; } Map get _jsonHeader => { ..._authHeader, 'Content-Type': 'application/json', }; Future status() => _get('/api/cloud/status'); Future syncStatus() => _get('/api/cloud/sync-status'); Future syncAll() => _post('/api/cloud/sync-all', {}); Future> listSongs() async { final r = await _get('/api/cloud/list'); return List.from(r?['songs'] ?? []); } Future upload(String filepath, String filename) async { try { final req = http.MultipartRequest('POST', Uri.parse('$_base/api/cloud/upload')); req.headers.addAll(_authHeader); req.files.add( await http.MultipartFile.fromPath('file', filepath, filename: filename)); final resp = await _client.send(req).timeout(const Duration(seconds: 120)); final body = jsonDecode(await resp.stream.bytesToString()); return body['song_id'] as String?; } catch (e) { MeloLogger().fehler('cloud_upload', e); return null; } } Future download(String songId, String destPath) async { try { final r = await _client .get(Uri.parse('$_base/api/cloud/download/$songId'), headers: _authHeader) .timeout(const Duration(seconds: 120)); if (r.statusCode == 200) { await File(destPath).writeAsBytes(r.bodyBytes); return true; } return false; } catch (e) { MeloLogger().fehler('cloud_download', e); return false; } } Future delete(String songId) async { try { final r = await _client .post(Uri.parse('$_base/api/cloud/delete'), headers: _jsonHeader, body: jsonEncode({'song_id': songId})) .timeout(const Duration(seconds: 10)); return r.statusCode == 200; } catch (_) { return false; } } /// Sync-Änderungen seit einem Zeitstempel abrufen Future> syncChanges(String since) async { try { final r = await _client .post(Uri.parse('$_base/api/cloud/sync'), headers: _jsonHeader, body: jsonEncode({'since': since})) .timeout(const Duration(seconds: 10)); if (r.statusCode == 200) { final d = jsonDecode(r.body); return List.from(d['changes'] ?? []); } } catch (_) {} return []; } // ─── 📋 Playlisten ─── /// Alle Playlisten des Users auf dem Server Future> getPlaylists() async { final r = await _get('/api/cloud/playlists'); return List.from(r?['playlists'] ?? []); } /// Playlist erstellen Future createPlaylist(String name) async { final r = await _post('/api/cloud/playlists', {'name': name}); return r?['playlist']; } /// Playlist löschen Future deletePlaylist(int id) async { final r = await _post('/api/cloud/playlists', {'id': id}); return r?['status'] == 'ok'; } /// Playlist-Details mit Songs abrufen Future getPlaylist(int id) async { return await _get('/api/cloud/playlists/$id'); } /// Songs zu Playlist hinzufügen Future addSongsToPlaylist(int playlistId, List songIds) async { final r = await _post( '/api/cloud/playlists/$playlistId/songs', {'song_ids': songIds}); return r?['added'] ?? 0; } /// Song aus Playlist entfernen Future removeSongFromPlaylist(int playlistId, String songId) async { try { final r = await _client .delete( Uri.parse( '$_base/api/cloud/playlists/$playlistId/songs/$songId'), headers: _authHeader) .timeout(const Duration(seconds: 10)); return r.statusCode == 200; } catch (_) { return false; } } /// Playlist-Positionen aktualisieren Future updatePlaylistPositions( int playlistId, List> positions) async { try { final r = await _client .put( Uri.parse( '$_base/api/cloud/playlists/$playlistId/positions'), headers: _jsonHeader, body: jsonEncode({'positions': positions})) .timeout(const Duration(seconds: 10)); return r.statusCode == 200; } catch (_) { return false; } } // ─── ⭐ Favoriten ─── /// Favoriten vom Server abrufen Future> getFavorites() async { final r = await _get('/api/cloud/favorites'); return List.from(r?['favorites'] ?? []); } /// Favoriten synchronisieren (komplette Liste senden) Future syncFavorites(List songIds) async { final r = await _post('/api/cloud/favorites', {'song_ids': songIds}); return r?['status'] == 'ok'; } /// Einzelnen Favoriten toggeln Future toggleFavorite(String songId) async { return await _post('/api/cloud/favorites/toggle', {'song_id': songId}); } // ─── ✏️ Umbenennen ─── /// Song umbenennen (benutzerdefinierter Titel/Artist) Future renameSong(String songId, {String? title, String? artist}) async { final body = {'song_id': songId}; if (title != null) body['title'] = title; if (artist != null) body['artist'] = artist; return await _post('/api/cloud/rename', body); } // ─── 🕐 Verlauf ─── /// Wiedergabe-Verlauf vom Server abrufen Future> getHistory({int limit = 50}) async { final r = await _get('/api/cloud/history?limit=$limit'); return List.from(r?['history'] ?? []); } /// Wiedergabe-Verlauf-Einträge hochladen Future addHistory(List> entries) async { final r = await _post('/api/cloud/history', {'entries': entries}); return r?['added'] ?? 0; } // ─── 🌐 Global / Shared ─── Future> globalList() async { try { final r = await _client .get(Uri.parse('$_base/api/cloud/global'), headers: _authHeader) .timeout(const Duration(seconds: 10)); if (r.statusCode == 200) { return List.from(jsonDecode(r.body)['songs'] ?? []); } } catch (_) {} return []; } Future toggleGlobal(String songId) async { try { final r = await _client .post(Uri.parse('$_base/api/cloud/toggle-global'), headers: _jsonHeader, body: jsonEncode({'song_id': songId})) .timeout(const Duration(seconds: 10)); return r.statusCode == 200; } catch (_) { return false; } } Future share(List songIds) async { try { final r = await _client .post(Uri.parse('$_base/api/cloud/share'), headers: _jsonHeader, body: jsonEncode({'song_ids': songIds})) .timeout(const Duration(seconds: 10)); if (r.statusCode == 200) { final d = jsonDecode(r.body); return d['code'] as String?; } } catch (_) {} return null; } Future importCode(String code) async { try { final r = await _client .post(Uri.parse('$_base/api/cloud/import'), headers: _jsonHeader, body: jsonEncode({'code': code})) .timeout(const Duration(seconds: 10)); if (r.statusCode == 200) return jsonDecode(r.body); } catch (_) {} return null; } Future> getCorrupted() async { try { final r = await _client .get(Uri.parse('$_base/api/cloud/corrupted'), headers: _authHeader) .timeout(const Duration(seconds: 15)); if (r.statusCode == 200) { final d = jsonDecode(r.body); return List.from(d['corrupted'] ?? []); } } catch (e) { MeloLogger().fehler('cloud_corrupted', e); } return []; } // ─── Hilfsmethoden ─── Future _get(String path) async { try { final r = await _client .get(Uri.parse('$_base$path'), headers: _authHeader) .timeout(const Duration(seconds: 10)); if (r.statusCode == 200) return jsonDecode(r.body); MeloLogger().fehler('cloud_get', '${r.statusCode} $path'); } catch (e) { MeloLogger().fehler('cloud_get_error', '$path: $e'); } return null; } Future _post(String path, Map body) async { try { final r = await _client .post(Uri.parse('$_base$path'), headers: _jsonHeader, body: jsonEncode(body)) .timeout(const Duration(seconds: 10)); if (r.statusCode == 200) return jsonDecode(r.body); MeloLogger().fehler('cloud_post', '${r.statusCode} $path'); } catch (e) { MeloLogger().fehler('cloud_post_error', '$path: $e'); } return null; } }