This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/services/cloud_service.dart
T

332 lines
10 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 '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
///
/// ## Konfliktauflösung: Last-Write-Wins (LWW)
/// Bei Sync-Konflikten (Client und Server haben beide geändert) gewinnt der
/// Eintrag mit dem neueren `updated_at`-Timestamp. Strategie:
/// 1. `syncChanges(since)` lädt serverseitige Änderungen seit letztem Sync.
/// 2. Client vergleicht `updated_at` mit lokalem Stand — Server gewinnt bei Gleichstand.
/// 3. Lokale Änderungen werden danach per `syncAll()` hochgeladen.
/// Merge-Strategie wird NICHT unterstützt (kein 3-Way-Merge) — es gewinnt immer
/// der jüngste Timestamp.
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<bool> login(String user) async {
try {
final r = await _client
.get(Uri.parse('$_base/api/v1/cloud/status'),
headers: _authHeader)
.timeout(const Duration(seconds: 5));
return r.statusCode == 200;
} catch (_) {
return false;
}
}
Map<String, String> get _authHeader {
final token = AuthService().token;
final headers = <String, String>{};
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
return headers;
}
Map<String, String> get _jsonHeader => {
..._authHeader,
'Content-Type': 'application/json',
};
Future<Map?> status() => _get('/api/v1/cloud/status');
Future<Map?> syncStatus() => _get('/api/v1/cloud/sync-status');
Future<Map?> syncAll() => _post('/api/v1/cloud/sync-all', {});
Future<List<Map>> listSongs() async {
final r = await _get('/api/v1/cloud/list');
return List<Map>.from(r?['songs'] ?? []);
}
Future<String?> upload(String filepath, String filename) async {
try {
final req =
http.MultipartRequest('POST', Uri.parse('$_base/api/v1/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<bool> download(String songId, String destPath) async {
try {
final r = await _client
.get(Uri.parse('$_base/api/v1/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<bool> delete(String songId) async {
try {
final r = await _client
.post(Uri.parse('$_base/api/v1/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<List<Map>> syncChanges(String since) async {
try {
final r = await _client
.post(Uri.parse('$_base/api/v1/cloud/sync'),
headers: _jsonHeader,
body: jsonEncode({'since': since}))
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) {
final d = jsonDecode(r.body);
return List<Map>.from(d['changes'] ?? []);
}
} catch (_) {}
return [];
}
// ─── 📋 Playlisten ───
/// Alle Playlisten des Users auf dem Server
Future<List<Map>> getPlaylists() async {
final r = await _get('/api/v1/cloud/playlists');
return List<Map>.from(r?['playlists'] ?? []);
}
/// Playlist erstellen
Future<Map?> createPlaylist(String name) async {
final r = await _post('/api/v1/cloud/playlists', {'name': name});
return r?['playlist'];
}
/// Playlist löschen
Future<bool> deletePlaylist(int id) async {
final r = await _post('/api/v1/cloud/playlists', {'id': id});
return r?['status'] == 'ok';
}
/// Playlist-Details mit Songs abrufen
Future<Map?> getPlaylist(int id) async {
return await _get('/api/v1/cloud/playlists/$id');
}
/// Songs zu Playlist hinzufügen
Future<int> addSongsToPlaylist(int playlistId, List<String> songIds) async {
final r = await _post(
'/api/v1/cloud/playlists/$playlistId/songs', {'song_ids': songIds});
return r?['added'] ?? 0;
}
/// Song aus Playlist entfernen
Future<bool> removeSongFromPlaylist(int playlistId, String songId) async {
try {
final r = await _client
.delete(
Uri.parse(
'$_base/api/v1/cloud/playlists/$playlistId/songs/$songId'),
headers: _authHeader)
.timeout(const Duration(seconds: 10));
return r.statusCode == 200;
} catch (_) {
return false;
}
}
/// Playlist-Positionen aktualisieren
Future<bool> updatePlaylistPositions(
int playlistId, List<Map<String, dynamic>> positions) async {
try {
final r = await _client
.put(
Uri.parse(
'$_base/api/v1/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<List<Map>> getFavorites() async {
final r = await _get('/api/v1/cloud/favorites');
return List<Map>.from(r?['favorites'] ?? []);
}
/// Favoriten synchronisieren (komplette Liste senden)
Future<bool> syncFavorites(List<String> songIds) async {
final r =
await _post('/api/v1/cloud/favorites', {'song_ids': songIds});
return r?['status'] == 'ok';
}
/// Einzelnen Favoriten toggeln
Future<Map?> toggleFavorite(String songId) async {
return await _post('/api/v1/cloud/favorites/toggle', {'song_id': songId});
}
// ─── ✏️ Umbenennen ───
/// Song umbenennen (benutzerdefinierter Titel/Artist)
Future<Map?> renameSong(String songId,
{String? title, String? artist}) async {
final body = <String, dynamic>{'song_id': songId};
if (title != null) body['title'] = title;
if (artist != null) body['artist'] = artist;
return await _post('/api/v1/cloud/rename', body);
}
// ─── 🕐 Verlauf ───
/// Wiedergabe-Verlauf vom Server abrufen
Future<List<Map>> getHistory({int limit = 50}) async {
final r = await _get('/api/v1/cloud/history?limit=$limit');
return List<Map>.from(r?['history'] ?? []);
}
/// Wiedergabe-Verlauf-Einträge hochladen
Future<int> addHistory(List<Map<String, dynamic>> entries) async {
final r = await _post('/api/v1/cloud/history', {'entries': entries});
return r?['added'] ?? 0;
}
// ─── 🌐 Global / Shared ───
Future<List<Map>> globalList() async {
try {
final r = await _client
.get(Uri.parse('$_base/api/v1/cloud/global'), headers: _authHeader)
.timeout(const Duration(seconds: 10));
if (r.statusCode == 200) {
return List<Map>.from(jsonDecode(r.body)['songs'] ?? []);
}
} catch (_) {}
return [];
}
Future<bool> toggleGlobal(String songId) async {
try {
final r = await _client
.post(Uri.parse('$_base/api/v1/cloud/toggle-global'),
headers: _jsonHeader,
body: jsonEncode({'song_id': songId}))
.timeout(const Duration(seconds: 10));
return r.statusCode == 200;
} catch (_) {
return false;
}
}
Future<String?> share(List<String> songIds) async {
try {
final r = await _client
.post(Uri.parse('$_base/api/v1/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<Map?> importCode(String code) async {
try {
final r = await _client
.post(Uri.parse('$_base/api/v1/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<List<Map>> getCorrupted() async {
try {
final r = await _client
.get(Uri.parse('$_base/api/v1/cloud/corrupted'), headers: _authHeader)
.timeout(const Duration(seconds: 15));
if (r.statusCode == 200) {
final d = jsonDecode(r.body);
return List<Map>.from(d['corrupted'] ?? []);
}
} catch (e) {
MeloLogger().fehler('cloud_corrupted', e);
}
return [];
}
// ─── Hilfsmethoden ───
Future<Map?> _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<Map?> _post(String path, Map<String, dynamic> 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;
}
}