fix: MEDIUM+LOW Claude-Audit-Findings (Issues #7-16)
MEDIUM: - #7 API-Key XOR-obfuskiert in app_config.dart (strings zeigen keinen Klartext) - #8 Navidrome-Download: Status-Code-Prüfung + hatValideMagicBytes() + istKorrupt - #9 _sucheYtUrls() sendet jetzt X-API-Key Header an YT-Proxy - #10 Log-Puffer auf 500 Einträge gecappt (älteste verwerfen) - #11 MeloLogger.cloudToken entfernt (war nie gesetzt, toter Code) - #12 TextEditingController-Leaks in 4 Files: ctrl.dispose() nach showDialog - #13 Cloud-Sync: Last-Write-Wins Konfliktauflösung dokumentiert LOW: - #14 API-Versionierung: alle /api/cloud/ → /api/v1/cloud/ - #15 Auto-Scan: nur /Music, /Download, nicht ganz /storage - #16 2 pre-existing flutter analyze Infos behoben (curly_braces, use_build_context_synchronously) flutter analyze: No issues found.
This commit is contained in:
@@ -7,6 +7,15 @@ 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();
|
||||
|
||||
@@ -16,7 +25,7 @@ class CloudService {
|
||||
Future<bool> login(String user) async {
|
||||
try {
|
||||
final r = await _client
|
||||
.get(Uri.parse('$_base/api/cloud/status'),
|
||||
.get(Uri.parse('$_base/api/v1/cloud/status'),
|
||||
headers: _authHeader)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
return r.statusCode == 200;
|
||||
@@ -41,21 +50,21 @@ class CloudService {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
Future<Map?> status() => _get('/api/cloud/status');
|
||||
Future<Map?> status() => _get('/api/v1/cloud/status');
|
||||
|
||||
Future<Map?> syncStatus() => _get('/api/cloud/sync-status');
|
||||
Future<Map?> syncStatus() => _get('/api/v1/cloud/sync-status');
|
||||
|
||||
Future<Map?> syncAll() => _post('/api/cloud/sync-all', {});
|
||||
Future<Map?> syncAll() => _post('/api/v1/cloud/sync-all', {});
|
||||
|
||||
Future<List<Map>> listSongs() async {
|
||||
final r = await _get('/api/cloud/list');
|
||||
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/cloud/upload'));
|
||||
http.MultipartRequest('POST', Uri.parse('$_base/api/v1/cloud/upload'));
|
||||
req.headers.addAll(_authHeader);
|
||||
req.files.add(
|
||||
await http.MultipartFile.fromPath('file', filepath,
|
||||
@@ -72,7 +81,7 @@ class CloudService {
|
||||
Future<bool> download(String songId, String destPath) async {
|
||||
try {
|
||||
final r = await _client
|
||||
.get(Uri.parse('$_base/api/cloud/download/$songId'),
|
||||
.get(Uri.parse('$_base/api/v1/cloud/download/$songId'),
|
||||
headers: _authHeader)
|
||||
.timeout(const Duration(seconds: 120));
|
||||
if (r.statusCode == 200) {
|
||||
@@ -89,7 +98,7 @@ class CloudService {
|
||||
Future<bool> delete(String songId) async {
|
||||
try {
|
||||
final r = await _client
|
||||
.post(Uri.parse('$_base/api/cloud/delete'),
|
||||
.post(Uri.parse('$_base/api/v1/cloud/delete'),
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'song_id': songId}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -103,7 +112,7 @@ class CloudService {
|
||||
Future<List<Map>> syncChanges(String since) async {
|
||||
try {
|
||||
final r = await _client
|
||||
.post(Uri.parse('$_base/api/cloud/sync'),
|
||||
.post(Uri.parse('$_base/api/v1/cloud/sync'),
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'since': since}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -119,31 +128,31 @@ class CloudService {
|
||||
|
||||
/// Alle Playlisten des Users auf dem Server
|
||||
Future<List<Map>> getPlaylists() async {
|
||||
final r = await _get('/api/cloud/playlists');
|
||||
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/cloud/playlists', {'name': name});
|
||||
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/cloud/playlists', {'id': id});
|
||||
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/cloud/playlists/$id');
|
||||
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/cloud/playlists/$playlistId/songs', {'song_ids': songIds});
|
||||
'/api/v1/cloud/playlists/$playlistId/songs', {'song_ids': songIds});
|
||||
return r?['added'] ?? 0;
|
||||
}
|
||||
|
||||
@@ -153,7 +162,7 @@ class CloudService {
|
||||
final r = await _client
|
||||
.delete(
|
||||
Uri.parse(
|
||||
'$_base/api/cloud/playlists/$playlistId/songs/$songId'),
|
||||
'$_base/api/v1/cloud/playlists/$playlistId/songs/$songId'),
|
||||
headers: _authHeader)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
return r.statusCode == 200;
|
||||
@@ -169,7 +178,7 @@ class CloudService {
|
||||
final r = await _client
|
||||
.put(
|
||||
Uri.parse(
|
||||
'$_base/api/cloud/playlists/$playlistId/positions'),
|
||||
'$_base/api/v1/cloud/playlists/$playlistId/positions'),
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'positions': positions}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -183,20 +192,20 @@ class CloudService {
|
||||
|
||||
/// Favoriten vom Server abrufen
|
||||
Future<List<Map>> getFavorites() async {
|
||||
final r = await _get('/api/cloud/favorites');
|
||||
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/cloud/favorites', {'song_ids': songIds});
|
||||
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/cloud/favorites/toggle', {'song_id': songId});
|
||||
return await _post('/api/v1/cloud/favorites/toggle', {'song_id': songId});
|
||||
}
|
||||
|
||||
// ─── ✏️ Umbenennen ───
|
||||
@@ -207,20 +216,20 @@ class CloudService {
|
||||
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);
|
||||
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/cloud/history?limit=$limit');
|
||||
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/cloud/history', {'entries': entries});
|
||||
final r = await _post('/api/v1/cloud/history', {'entries': entries});
|
||||
return r?['added'] ?? 0;
|
||||
}
|
||||
|
||||
@@ -229,7 +238,7 @@ class CloudService {
|
||||
Future<List<Map>> globalList() async {
|
||||
try {
|
||||
final r = await _client
|
||||
.get(Uri.parse('$_base/api/cloud/global'), headers: _authHeader)
|
||||
.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'] ?? []);
|
||||
@@ -241,7 +250,7 @@ class CloudService {
|
||||
Future<bool> toggleGlobal(String songId) async {
|
||||
try {
|
||||
final r = await _client
|
||||
.post(Uri.parse('$_base/api/cloud/toggle-global'),
|
||||
.post(Uri.parse('$_base/api/v1/cloud/toggle-global'),
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'song_id': songId}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -254,7 +263,7 @@ class CloudService {
|
||||
Future<String?> share(List<String> songIds) async {
|
||||
try {
|
||||
final r = await _client
|
||||
.post(Uri.parse('$_base/api/cloud/share'),
|
||||
.post(Uri.parse('$_base/api/v1/cloud/share'),
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'song_ids': songIds}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -269,7 +278,7 @@ class CloudService {
|
||||
Future<Map?> importCode(String code) async {
|
||||
try {
|
||||
final r = await _client
|
||||
.post(Uri.parse('$_base/api/cloud/import'),
|
||||
.post(Uri.parse('$_base/api/v1/cloud/import'),
|
||||
headers: _jsonHeader,
|
||||
body: jsonEncode({'code': code}))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -281,7 +290,7 @@ class CloudService {
|
||||
Future<List<Map>> getCorrupted() async {
|
||||
try {
|
||||
final r = await _client
|
||||
.get(Uri.parse('$_base/api/cloud/corrupted'), headers: _authHeader)
|
||||
.get(Uri.parse('$_base/api/v1/cloud/corrupted'), headers: _authHeader)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
if (r.statusCode == 200) {
|
||||
final d = jsonDecode(r.body);
|
||||
|
||||
Reference in New Issue
Block a user