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
Dustin 15993b1d0a v2.35 — Vollständiges Sync-System: Playlisten, Favoriten, Auto-Sync, Persistent Login, Benutzerdefinierte Namen
## Server (melo_cloud.py v3.0.0)
- Neue DB-Tabellen: user_playlists, user_playlist_songs, user_favorites, user_history, sync_meta
- Playlist-Endpunkte: GET/POST /api/cloud/playlists, GET /api/cloud/playlists/<id>, POST songs, DELETE songs, PUT positions
- Favoriten-Endpunkte: GET/POST /api/cloud/favorites, POST /api/cloud/favorites/toggle
- History-Endpunkte: GET/POST /api/cloud/history (mit Dedup)
- Rename-Endpunkt: POST /api/cloud/rename (custom_title/custom_artist)
- Sync-Endpunkte: POST /api/cloud/sync-all, GET /api/cloud/sync-status
- delete-all erweitert um Playlists + Favorites + History

## App
- db_helper.dart: Migration v4→v5 (cloud_id, cloud_title, cloud_artist, server_favorites, sync_metadata)
- auth_service.dart: Token-Validierung beim Start (online check + Offline-Fallback), Persistent Login
- cloud_service.dart: Neue Methoden für Playlists (CRUD), Favorites (get/sync/toggle), Rename, History, syncAll/syncStatus
- cloud_screen.dart: Komplett-Rewrite mit Sync-Modus (Manuell/Auto), Intervall-Auswahl, Sync-Fortschritt-Anzeige, Playlist-Verwaltung, Favoriten-Anzeige + Umbenennen, Sync-Info-Karte, nächster Sync
- main.dart: Auto-Sync beim App-Start (nur wenn konfiguriert + fällig)

## Flutter Analyze
- 0 neue Issues — nur 7 pre-existing
2026-08-01 19:32:35 +02:00

328 lines
9.7 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
class CloudService {
static String get _base => AppConfig.cloudUrl;
String _user = '';
/// Login mit Baka-Auth Token wird aus AuthService bezogen
Future<bool> login(String user) async {
_user = user;
try {
final r = await http
.get(Uri.parse('$_base/api/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>{
'X-API-Key': AppConfig.ytProxyApiKey,
};
if (_user.isNotEmpty) {
headers['X-User'] = _user;
}
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/cloud/status');
Future<Map?> syncStatus() => _get('/api/cloud/sync-status');
Future<Map?> syncAll() => _post('/api/cloud/sync-all', {});
Future<List<Map>> listSongs() async {
final r = await _get('/api/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/cloud/upload'));
req.headers.addAll(_authHeader);
req.files.add(
await http.MultipartFile.fromPath('file', filepath,
filename: filename));
final resp = await req.send().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 http
.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<bool> delete(String songId) async {
try {
final r = await http
.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<List<Map>> syncChanges(String since) async {
try {
final r = await http
.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<Map>.from(d['changes'] ?? []);
}
} catch (_) {}
return [];
}
// ─── 📋 Playlisten ───
/// Alle Playlisten des Users auf dem Server
Future<List<Map>> getPlaylists() async {
final r = await _get('/api/cloud/playlists');
return List<Map>.from(r?['playlists'] ?? []);
}
/// Playlist erstellen
Future<Map?> createPlaylist(String name) async {
final r = await _post('/api/cloud/playlists', {'name': name});
return r?['playlist'];
}
/// Playlist löschen
Future<bool> deletePlaylist(int id) async {
final r = await _post('/api/cloud/playlists', {'id': id});
return r?['status'] == 'ok';
}
/// Playlist-Details mit Songs abrufen
Future<Map?> getPlaylist(int id) async {
return await _get('/api/cloud/playlists/$id');
}
/// Songs zu Playlist hinzufügen
Future<int> addSongsToPlaylist(int playlistId, List<String> songIds) async {
final r = await _post(
'/api/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 http
.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<bool> updatePlaylistPositions(
int playlistId, List<Map<String, dynamic>> positions) async {
try {
final r = await http
.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<List<Map>> getFavorites() async {
final r = await _get('/api/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/cloud/favorites', {'song_ids': songIds});
return r?['status'] == 'ok';
}
/// Einzelnen Favoriten toggeln
Future<Map?> toggleFavorite(String songId) async {
return await _post('/api/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/cloud/rename', body);
}
// ─── 🕐 Verlauf ───
/// Wiedergabe-Verlauf vom Server abrufen
Future<List<Map>> getHistory({int limit = 50}) async {
final r = await _get('/api/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/cloud/history', {'entries': entries});
return r?['added'] ?? 0;
}
// ─── 🌐 Global / Shared ───
Future<List<Map>> globalList() async {
try {
final r = await http
.get(Uri.parse('$_base/api/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 http
.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<String?> share(List<String> songIds) async {
try {
final r = await http
.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<Map?> importCode(String code) async {
try {
final r = await http
.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<List<Map>> getCorrupted() async {
try {
final r = await http
.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<Map>.from(d['corrupted'] ?? []);
}
} catch (e) {
MeloLogger().fehler('cloud_corrupted', e);
}
return [];
}
// ─── Hilfsmethoden ───
Future<Map?> _get(String path) async {
try {
final r = await http
.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 http
.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;
}
}