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
This commit is contained in:
@@ -20,7 +20,7 @@ class DbHelper {
|
||||
final pfad = await getDatabasesPath();
|
||||
return openDatabase(
|
||||
p.join(pfad, 'melo.db'),
|
||||
version: 4,
|
||||
version: 5,
|
||||
onCreate: (db, version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE songs (
|
||||
@@ -107,6 +107,33 @@ class DbHelper {
|
||||
// Spalte existiert bereits – ignorieren
|
||||
}
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
try {
|
||||
await db.execute('ALTER TABLE songs ADD COLUMN cloud_id TEXT');
|
||||
} catch (_) {}
|
||||
try {
|
||||
await db.execute('ALTER TABLE songs ADD COLUMN cloud_title TEXT');
|
||||
} catch (_) {}
|
||||
try {
|
||||
await db.execute('ALTER TABLE songs ADD COLUMN cloud_artist TEXT');
|
||||
} catch (_) {}
|
||||
try {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS server_favorites (
|
||||
cloud_id TEXT PRIMARY KEY,
|
||||
favorited_at TEXT
|
||||
)
|
||||
''');
|
||||
} catch (_) {}
|
||||
try {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sync_metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)
|
||||
''');
|
||||
} catch (_) {}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -356,6 +383,73 @@ class DbHelper {
|
||||
return rows.map((r) => Song.fromMap(r)).toList();
|
||||
}
|
||||
|
||||
/// Cloud-ID für einen Song speichern (Verknüpfung lokal ↔ Cloud)
|
||||
Future<void> cloudIdSetzen(int songId, String cloudId,
|
||||
{String? cloudTitle, String? cloudArtist}) async {
|
||||
final d = await db;
|
||||
final update = <String, dynamic>{'cloud_id': cloudId};
|
||||
if (cloudTitle != null) update['cloud_title'] = cloudTitle;
|
||||
if (cloudArtist != null) update['cloud_artist'] = cloudArtist;
|
||||
await d.update('songs', update, where: 'id = ?', whereArgs: [songId]);
|
||||
}
|
||||
|
||||
/// Sync-Metadaten lesen
|
||||
Future<String?> syncMetaGet(String key) async {
|
||||
final d = await db;
|
||||
final rows = await d.query('sync_metadata',
|
||||
where: 'key = ?', whereArgs: [key]);
|
||||
if (rows.isEmpty) return null;
|
||||
return rows.first['value'] as String?;
|
||||
}
|
||||
|
||||
/// Sync-Metadaten setzen
|
||||
Future<void> syncMetaSet(String key, String value) async {
|
||||
final d = await db;
|
||||
await d.insert('sync_metadata', {'key': key, 'value': value},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
/// Server-Favoriten abrufen (alle cloud_ids)
|
||||
Future<Set<String>> serverFavoritesGet() async {
|
||||
final d = await db;
|
||||
final rows = await d.query('server_favorites');
|
||||
return rows.map((r) => r['cloud_id'] as String).toSet();
|
||||
}
|
||||
|
||||
/// Server-Favoriten ersetzen (komplette Liste)
|
||||
Future<void> serverFavoritesSet(List<String> cloudIds) async {
|
||||
final d = await db;
|
||||
await d.transaction((txn) async {
|
||||
await txn.delete('server_favorites');
|
||||
final now = DateTime.now().toIso8601String();
|
||||
for (final cid in cloudIds) {
|
||||
await txn.insert('server_favorites',
|
||||
{'cloud_id': cid, 'favorited_at': now});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Song per cloud_id finden
|
||||
Future<Song?> songNachCloudId(String cloudId) async {
|
||||
final d = await db;
|
||||
final rows = await d.query('songs',
|
||||
where: 'cloud_id = ?', whereArgs: [cloudId]);
|
||||
if (rows.isEmpty) return null;
|
||||
return Song.fromMap(rows.first);
|
||||
}
|
||||
|
||||
/// Benutzerdefinierte Metadaten für Cloud-Song aktualisieren
|
||||
Future<void> cloudMetadatenAktualisieren(int songId,
|
||||
{String? title, String? artist}) async {
|
||||
final d = await db;
|
||||
final update = <String, dynamic>{};
|
||||
if (title != null) update['cloud_title'] = title;
|
||||
if (artist != null) update['cloud_artist'] = artist;
|
||||
if (update.isNotEmpty) {
|
||||
await d.update('songs', update, where: 'id = ?', whereArgs: [songId]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loeschen() async {
|
||||
final d = await db;
|
||||
await d.transaction((txn) async {
|
||||
|
||||
+51
-2
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'database/db_helper.dart';
|
||||
import 'services/favoriten_service.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'services/cloud_service.dart';
|
||||
import 'services/melo_logger.dart';
|
||||
import 'services/audio_handler.dart';
|
||||
import 'utils/farb_theme.dart';
|
||||
@@ -13,7 +15,7 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Logger startet sofort – zeichnet ALLES auf
|
||||
MeloLogger().init('2.31');
|
||||
MeloLogger().init('2.35');
|
||||
|
||||
try {
|
||||
await DbHelper().db;
|
||||
@@ -32,12 +34,59 @@ void main() async {
|
||||
MeloLogger().fehler('App-Start', e, stack);
|
||||
}
|
||||
|
||||
// Auth initialisieren (Token aus SharedPreferences laden)
|
||||
// Auth initialisieren (Token aus SharedPreferences laden + online validieren)
|
||||
await AuthService().initialisieren();
|
||||
|
||||
// Auto-Sync beim Start (wenn eingeloggt und Auto-Sync aktiv)
|
||||
_starteAutoSyncFallsNoetig();
|
||||
|
||||
runApp(const MeloApp());
|
||||
}
|
||||
|
||||
/// Führt Cloud-Auto-Sync beim App-Start aus, falls konfiguriert
|
||||
Future<void> _starteAutoSyncFallsNoetig() async {
|
||||
try {
|
||||
final auth = AuthService();
|
||||
if (!auth.istEingeloggt) return;
|
||||
|
||||
// Prüfen, ob Auto-Sync aktiviert ist
|
||||
final SharedPreferences prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
final autoSync = prefs.getBool('cloud_auto') ?? true;
|
||||
if (!autoSync) return;
|
||||
|
||||
final cloud = CloudService();
|
||||
final ok = await cloud.login(auth.benutzer);
|
||||
if (!ok) return;
|
||||
|
||||
// Letzten Sync prüfen — nur syncen wenn nötig
|
||||
final lastSync = prefs.getString('cloud_last_sync_ts');
|
||||
final now = DateTime.now();
|
||||
|
||||
if (lastSync != null) {
|
||||
final last = DateTime.tryParse(lastSync);
|
||||
if (last != null) {
|
||||
final interval = prefs.getInt('cloud_interval') ?? 6;
|
||||
if (now.difference(last).inHours < interval) {
|
||||
return; // Noch nicht fällig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync ausführen
|
||||
MeloLogger().aktion('auto_sync_start', {});
|
||||
final st = await cloud.status();
|
||||
if (st != null) {
|
||||
final serverSongs = await cloud.listSongs();
|
||||
MeloLogger().aktion('auto_sync_done',
|
||||
{'songs': serverSongs.length});
|
||||
}
|
||||
await prefs.setString('cloud_last_sync_ts', now.toIso8601String());
|
||||
} catch (e) {
|
||||
MeloLogger().fehler('auto_sync_init', e);
|
||||
}
|
||||
}
|
||||
|
||||
class MeloApp extends StatelessWidget {
|
||||
const MeloApp({super.key});
|
||||
|
||||
|
||||
+1177
-384
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../services/melo_logger.dart';
|
||||
|
||||
/// Baka-Auth Service – JWT-basierte Authentifizierung
|
||||
/// Baka-Auth Service – JWT-basierte Authentifizierung (v3 — Persistent Login)
|
||||
/// Integriert mit https://baka-net.de/auth
|
||||
class AuthService {
|
||||
static final AuthService _instance = AuthService._();
|
||||
@@ -31,24 +30,53 @@ class AuthService {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/// Lädt gespeicherten Token beim App-Start
|
||||
Future<void> initialisieren() async {
|
||||
if (_initialisiert) return;
|
||||
/// Lädt gespeicherten Token beim App-Start und validiert ihn
|
||||
Future<bool> initialisieren() async {
|
||||
if (_initialisiert) return istEingeloggt;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_token = prefs.getString('baka_token');
|
||||
_user = prefs.getString('baka_user') ?? '';
|
||||
|
||||
if (_token != null && _token!.isNotEmpty) {
|
||||
MeloLogger().zustand('auth_restored', {'user': _user});
|
||||
// Token beim Server validieren (nicht blind vertrauen)
|
||||
final valid = await _tokenOnlinePruefen();
|
||||
if (!valid) {
|
||||
// Token ist abgelaufen — nicht als eingeloggt behandeln
|
||||
MeloLogger().zustand('auth_token_expired', {'user': _user});
|
||||
_token = null;
|
||||
_user = '';
|
||||
await prefs.remove('baka_token');
|
||||
await prefs.remove('baka_user');
|
||||
} else {
|
||||
MeloLogger().zustand('auth_restored', {'user': _user});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
MeloLogger().fehler('auth_init', e);
|
||||
}
|
||||
_initialisiert = true;
|
||||
return istEingeloggt;
|
||||
}
|
||||
|
||||
/// Prüft Token beim Baka-Auth-Server (online)
|
||||
Future<bool> _tokenOnlinePruefen() async {
|
||||
if (_token == null) return false;
|
||||
try {
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('${AppConfig.authUrl}/verify'),
|
||||
headers: authHeader,
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
return response.statusCode == 200;
|
||||
} catch (_) {
|
||||
// Bei Netzwerkfehler: Token lokal akzeptieren (Offline-Modus)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Login über Baka-Auth-Server
|
||||
/// Gibt true zurück bei Erfolg, false bei Fehler
|
||||
Future<AuthResult> login(String user, String password) async {
|
||||
try {
|
||||
final response = await http
|
||||
@@ -74,14 +102,13 @@ class AuthService {
|
||||
MeloLogger().aktion('auth_login_ok', {'user': _user});
|
||||
return AuthResult.ok;
|
||||
}
|
||||
// 200 aber kein Token → Fehler vom Server
|
||||
final serverMsg = data['message'] as String? ?? data['error'] as String? ?? 'Unbekannt';
|
||||
final serverMsg = data['message'] as String? ??
|
||||
data['error'] as String? ??
|
||||
'Unbekannt';
|
||||
return AuthResult.fehlgeschlagen(serverMsg);
|
||||
}
|
||||
|
||||
// Nicht-200
|
||||
String fehler = 'Login fehlgeschlagen (${response.statusCode})';
|
||||
|
||||
MeloLogger().fehler('auth_login_fail', fehler);
|
||||
return AuthResult.fehlgeschlagen(fehler);
|
||||
} catch (e) {
|
||||
@@ -92,7 +119,8 @@ class AuthService {
|
||||
}
|
||||
|
||||
/// Registrierung über Baka-Auth-Server
|
||||
Future<AuthResult> registrieren(String user, String password, String email) async {
|
||||
Future<AuthResult> registrieren(
|
||||
String user, String password, String email) async {
|
||||
try {
|
||||
final response = await http
|
||||
.post(
|
||||
@@ -115,7 +143,6 @@ class AuthService {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('baka_token', _token!);
|
||||
await prefs.setString('baka_user', _user);
|
||||
|
||||
MeloLogger().aktion('auth_register_ok', {'user': _user});
|
||||
return AuthResult.ok;
|
||||
}
|
||||
@@ -126,27 +153,15 @@ class AuthService {
|
||||
final data = jsonDecode(response.body);
|
||||
fehler = data['error'] as String? ?? fehler;
|
||||
} catch (_) {}
|
||||
|
||||
return AuthResult.fehlgeschlagen(fehler);
|
||||
} catch (e) {
|
||||
return AuthResult.fehlgeschlagen('Keine Verbindung zum Server');
|
||||
}
|
||||
}
|
||||
|
||||
/// Token beim Server validieren
|
||||
/// Token beim Server validieren (public)
|
||||
Future<bool> tokenPruefen() async {
|
||||
if (_token == null) return false;
|
||||
try {
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('${AppConfig.authUrl}/verify'),
|
||||
headers: authHeader,
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
return response.statusCode == 200;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return _tokenOnlinePruefen();
|
||||
}
|
||||
|
||||
/// Ausloggen – Token löschen
|
||||
|
||||
+186
-49
@@ -5,7 +5,7 @@ import '../config/app_config.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/melo_logger.dart';
|
||||
|
||||
/// Cloud-Sync Service für Melo Registry
|
||||
/// 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;
|
||||
@@ -33,15 +33,23 @@ class CloudService {
|
||||
if (_user.isNotEmpty) {
|
||||
headers['X-User'] = _user;
|
||||
}
|
||||
// Baka-Auth JWT Token mitsenden falls vorhanden
|
||||
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'] ?? []);
|
||||
@@ -49,10 +57,12 @@ class CloudService {
|
||||
|
||||
Future<String?> upload(String filepath, String filename) async {
|
||||
try {
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_base/api/cloud/upload'));
|
||||
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));
|
||||
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?;
|
||||
@@ -83,7 +93,7 @@ class CloudService {
|
||||
try {
|
||||
final r = await http
|
||||
.post(Uri.parse('$_base/api/cloud/delete'),
|
||||
headers: {..._authHeader, 'Content-Type': 'application/json'},
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'song_id': songId}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
return r.statusCode == 200;
|
||||
@@ -92,51 +102,12 @@ class CloudService {
|
||||
}
|
||||
}
|
||||
|
||||
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<String?> share(List<String> songIds) async {
|
||||
try {
|
||||
final r = await http
|
||||
.post(Uri.parse('$_base/api/cloud/share'),
|
||||
headers: {..._authHeader, 'Content-Type': 'application/json'},
|
||||
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: {..._authHeader, 'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'code': code}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (r.statusCode == 200) return jsonDecode(r.body);
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 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: {..._authHeader, 'Content-Type': 'application/json'},
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'since': since}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (r.statusCode == 200) {
|
||||
@@ -147,6 +118,117 @@ class CloudService {
|
||||
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
|
||||
@@ -163,7 +245,7 @@ class CloudService {
|
||||
try {
|
||||
final r = await http
|
||||
.post(Uri.parse('$_base/api/cloud/toggle-global'),
|
||||
headers: {..._authHeader, 'Content-Type': 'application/json'},
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'song_id': songId}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
return r.statusCode == 200;
|
||||
@@ -172,7 +254,33 @@ class CloudService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ruft defekte/korrupte Songs vom Cloud-Server ab
|
||||
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
|
||||
@@ -187,4 +295,33 @@ class CloudService {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user