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/navidrome_service.dart
T

185 lines
5.4 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import '../models/song.dart';
import '../database/db_helper.dart';
/// Ein Song aus der Subsonic-API
class SubsonicSong {
final String id;
final String titel;
final String kuenstler;
final String album;
final int dauerSekunden;
final int groesseBytes;
final String? coverId;
const SubsonicSong({
required this.id,
required this.titel,
this.kuenstler = '',
this.album = '',
this.dauerSekunden = 0,
this.groesseBytes = 0,
this.coverId,
});
factory SubsonicSong.fromJson(Map<String, dynamic> j) => SubsonicSong(
id: j['id'] as String,
titel: j['title'] as String? ?? 'Unbekannt',
kuenstler: j['artist'] as String? ?? '',
album: j['album'] as String? ?? '',
dauerSekunden: (j['duration'] as int?) ?? 0,
groesseBytes: (j['size'] as int?) ?? 0,
coverId: j['coverArt'] as String?,
);
}
/// Ein Album aus der Subsonic-API
class SubsonicAlbum {
final String id;
final String name;
final String? coverId;
final int songCount;
const SubsonicAlbum({
required this.id,
required this.name,
this.coverId,
this.songCount = 0,
});
factory SubsonicAlbum.fromJson(Map<String, dynamic> j) => SubsonicAlbum(
id: j['id'] as String,
name: j['name'] as String? ?? j['title'] as String? ?? 'Unbekannt',
coverId: j['coverArt'] as String?,
songCount: (j['songCount'] as int?) ?? 0,
);
}
class NavidromeService {
final DbHelper _db = DbHelper();
String _serverUrl = '';
String _user = '';
String _password = '';
String _salt = '';
String? _token;
static const _version = '1.16.1';
static const _client = 'Melo';
void setCredentials(String url, String user, String password) {
_serverUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
_user = user;
_password = password;
_salt = DateTime.now().millisecondsSinceEpoch.toString();
_token = md5.convert(utf8.encode(_password + _salt)).toString();
}
bool get istVerbunden => _serverUrl.isNotEmpty && _user.isNotEmpty;
Uri _uri(String endpoint, [Map<String, String>? extra]) {
final params = {
'u': _user,
't': _token!,
's': _salt,
'v': _version,
'c': _client,
'f': 'json',
};
if (extra != null) params.addAll(extra);
return Uri.parse('$_serverUrl/rest/$endpoint').replace(queryParameters: params);
}
/// Verbindung testen
Future<bool> ping() async {
try {
final r = await http.get(_uri('ping.view')).timeout(const Duration(seconds: 10));
return r.statusCode == 200;
} catch (_) {
return false;
}
}
/// Alle Alben abrufen
Future<List<SubsonicAlbum>> getAlben({int anzahl = 50}) async {
final r = await http.get(_uri('getAlbumList.view', {'type': 'newest', 'size': '$anzahl'})).timeout(const Duration(seconds: 15));
if (r.statusCode != 200) return [];
final data = jsonDecode(r.body);
final list = data['subsonic-response']?['albumList']?['album'] as List? ?? [];
return list.map((j) => SubsonicAlbum.fromJson(j)).toList();
}
/// Songs eines Albums abrufen
Future<List<SubsonicSong>> getSongs(String albumId) async {
final r = await http.get(_uri('getAlbum.view', {'id': albumId})).timeout(const Duration(seconds: 15));
if (r.statusCode != 200) return [];
final data = jsonDecode(r.body);
final songs = data['subsonic-response']?['album']?['song'] as List? ?? [];
return songs.map((j) => SubsonicSong.fromJson(j)).toList();
}
/// Song-Stream-URL
Uri streamUrl(String songId) {
return _uri('stream.view', {'id': songId, 'format': 'raw'});
}
/// Cover-URL
Uri? coverUrl(String coverId) {
return _uri('getCoverArt.view', {'id': coverId});
}
/// Song von Navidrome runterladen und lokal speichern
Future<Song?> downloadSong(SubsonicSong s) async {
try {
final dir = await getApplicationDocumentsDirectory();
final musikDir = Directory('${dir.path}/music');
if (!await musikDir.exists()) await musikDir.create(recursive: true);
final safeName = s.titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim();
final dateiName = '${safeName.isEmpty ? "song" : safeName}.mp3';
final dateiPfad = '${musikDir.path}/$dateiName';
final file = File(dateiPfad);
if (await file.exists()) {
final localSong = await _db.songNachPfad(dateiPfad);
if (localSong != null) return localSong;
}
// Stream + speichern
final uri = streamUrl(s.id);
final response = await http.Client().send(http.Request('GET', uri));
final sink = file.openWrite();
await for (final chunk in response.stream) {
sink.add(chunk);
}
await sink.flush();
await sink.close();
final song = Song(
titel: s.titel,
kuenstler: s.kuenstler,
album: s.album,
dauerSekunden: s.dauerSekunden,
dateiPfad: dateiPfad,
groesseBytes: await file.length(),
istHeruntergeladen: true,
downloadQuelle: 'server',
);
// Dauer per just_audio ermitteln wenn Navidrome keine liefert
// (optional, für später)
await _db.songEinfuegen(song);
return song;
} catch (e) {
debugPrint('Navidrome Download Fehler: $e');
return null;
}
}
}