2024-05-23 19:14:08 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
import 'package:ocarina/api/subsonic/album.dart';
|
|
|
|
import 'package:ocarina/api/subsonic/artistindex.dart';
|
|
|
|
import 'package:ocarina/api/subsonic/song.dart';
|
|
|
|
import 'package:ocarina/util/util.dart';
|
|
|
|
|
|
|
|
/// Used to communicate with Subsonic API
|
|
|
|
class SubsonicApiService {
|
|
|
|
/// Used to communicate with Subsonic API
|
|
|
|
|
|
|
|
factory SubsonicApiService() {
|
|
|
|
return _subsonicService;
|
|
|
|
}
|
|
|
|
|
|
|
|
SubsonicApiService._internal();
|
|
|
|
static final SubsonicApiService _subsonicService =
|
|
|
|
SubsonicApiService._internal();
|
|
|
|
|
|
|
|
Dio get _client =>
|
|
|
|
Dio(BaseOptions(contentType: "application/json", baseUrl: baseUrl));
|
|
|
|
|
|
|
|
// ignore: avoid_setters_without_getters
|
|
|
|
set password(String p) {
|
|
|
|
_salt = Util.makeSalt;
|
|
|
|
_hash = md5.convert(utf8.encode(p + _salt)).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Response<Map<String, dynamic>>> _get(
|
|
|
|
String path, {
|
|
|
|
Map<String, dynamic> params = const {},
|
|
|
|
}) async {
|
|
|
|
return _client.get<Map<String, dynamic>>(
|
|
|
|
path,
|
|
|
|
queryParameters: {
|
|
|
|
"u": user,
|
|
|
|
"t": _hash,
|
|
|
|
"s": _salt,
|
|
|
|
"c": "Ocarina",
|
|
|
|
"f": "json",
|
|
|
|
"v": _version,
|
|
|
|
...params,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Subsonic-compatible server's base URL
|
|
|
|
String baseUrl = "";
|
|
|
|
|
|
|
|
/// Subsonic username
|
|
|
|
String user = "";
|
|
|
|
String _hash = "";
|
|
|
|
final String _version = "1.16.1";
|
|
|
|
String _salt = "";
|
|
|
|
|
|
|
|
/// Checks if saved details return an OK from server
|
|
|
|
Future<bool> verifyCredentials() async {
|
|
|
|
try {
|
|
|
|
final r = await _get("/rest/ping.view");
|
|
|
|
if (r.data == null || r.data!.containsKey("error")) return false;
|
|
|
|
return true;
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<ArtistIndex>?> getArtistIndex() async {
|
|
|
|
try {
|
|
|
|
final r = await _get("/rest/getIndexes");
|
|
|
|
if (r.data == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
final response = r.data!["subsonic-response"] as Map<String, dynamic>;
|
|
|
|
if (response["status"] != "ok") return null;
|
|
|
|
|
|
|
|
final indexes = List<Map<String, dynamic>>.from(
|
|
|
|
(response["indexes"] as Map<String, dynamic>)["index"] as List<dynamic>,
|
|
|
|
);
|
|
|
|
return List<ArtistIndex>.generate(
|
|
|
|
indexes.length,
|
|
|
|
(index) => ArtistIndex.fromJson(indexes[index]),
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
logger.e(e);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns data from the `getMusicDirectory` endpoint formatted as [Album]
|
|
|
|
///
|
|
|
|
/// Checks if it is really a directory and tries to format it with fromJson
|
|
|
|
Future<List<Album>?> getAlbumList(String artistId) async {
|
|
|
|
final r = await _get("/rest/getMusicDirectory", params: {"id": artistId});
|
|
|
|
if (r.data == null || r.data!["subsonic-response"]["status"] != "ok") {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
final albums = <Album>[];
|
|
|
|
for (final albumData in r.data!["subsonic-response"]["directory"]["child"]
|
|
|
|
as List<dynamic>) {
|
|
|
|
if ((albumData as Map<String, dynamic>)["isDir"] == false) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
albums.add(Album.fromJson(albumData));
|
|
|
|
} catch (e) {
|
2024-05-24 00:10:11 +02:00
|
|
|
logger
|
|
|
|
..e("Error for $artistId")
|
|
|
|
..e(e)
|
|
|
|
..e(albumData);
|
2024-05-23 19:14:08 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return albums;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns data from the `getMusicDirectory` endpoint formatted as [Album]
|
|
|
|
///
|
|
|
|
/// Checks if it is really a directory and tries to format it with fromJson
|
|
|
|
Future<List<Song>?> getSongList(String albumId) async {
|
|
|
|
final r = await _get("/rest/getMusicDirectory", params: {"id": albumId});
|
|
|
|
if (r.data == null || r.data!["subsonic-response"]["status"] != "ok") {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
final songs = <Song>[];
|
|
|
|
for (final songData in r.data!["subsonic-response"]["directory"]["child"]
|
|
|
|
as List<dynamic>) {
|
|
|
|
if ((songData as Map<String, dynamic>)["isDir"] == true) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
songs.add(Song.fromJson(songData));
|
|
|
|
} catch (e) {
|
2024-05-24 00:10:11 +02:00
|
|
|
logger
|
|
|
|
..e("Error for $albumId")
|
|
|
|
..e(e)
|
|
|
|
..e(songData);
|
2024-05-23 19:14:08 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return songs;
|
|
|
|
}
|
|
|
|
|
|
|
|
String getCoverArtUrl(String id) =>
|
|
|
|
"$baseUrl/rest/getCoverArt?id=$id&u=$user&s=$_salt&t=$_hash&c=Ocarina&v=1.16.1";
|
|
|
|
String getStreamUrl(String id) =>
|
|
|
|
"$baseUrl/rest/stream?id=$id&u=$user&s=$_salt&t=$_hash&c=Ocarina&v=1.16.1";
|
|
|
|
}
|