mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
improve album tests
This commit is contained in:
parent
c900c9750a
commit
2df86f4faa
@ -13,7 +13,7 @@ const client = new SubsonicClient(
|
||||
);
|
||||
|
||||
for (const id of ["197", "199", "321"]) {
|
||||
const res = await client.get("download", { id });
|
||||
const { res } = await client.get("download", { id });
|
||||
|
||||
let filename = res.headers.get("Content-Disposition")
|
||||
?.split(";")[1];
|
||||
|
||||
@ -1,18 +1,65 @@
|
||||
#!/usr/bin/env -S deno --allow-all
|
||||
import { SubsonicClient } from "./util/subsonic.ts";
|
||||
import { sleep } from "./util/util.ts";
|
||||
|
||||
async function scrobbleTrack(
|
||||
client: SubsonicClient,
|
||||
album: string,
|
||||
track: number,
|
||||
) {
|
||||
const { xml: albumsXml } = await client.get("getAlbumList2", {
|
||||
type: "newest",
|
||||
});
|
||||
const albumId = albumsXml.querySelector(`album[name='${album}']`)?.id;
|
||||
|
||||
const { xml: songsXml } = await client.get("getAlbum", { id: albumId! });
|
||||
const songId = songsXml.querySelector(`song[track='${track}']`)?.id;
|
||||
|
||||
await client.get("scrobble", {
|
||||
id: songId!,
|
||||
submission: "true",
|
||||
});
|
||||
}
|
||||
|
||||
async function setupTestData(client: SubsonicClient) {
|
||||
await scrobbleTrack(client, "Retroconnaissance EP", 1);
|
||||
await sleep(1_000);
|
||||
await scrobbleTrack(client, "Retroconnaissance EP", 2);
|
||||
await sleep(1_000);
|
||||
await scrobbleTrack(client, "Kosmonaut", 1);
|
||||
}
|
||||
|
||||
async function setupNavidrome() {
|
||||
console.log("setting up navidrome...");
|
||||
|
||||
const baseUrl = "http://navidrome:4533";
|
||||
const username = "admin";
|
||||
const password = "password";
|
||||
|
||||
await fetch("http://navidrome:4533/auth/createAdmin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: "admin",
|
||||
password: "password",
|
||||
}),
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const client = new SubsonicClient(baseUrl, username, password);
|
||||
await setupTestData(client);
|
||||
}
|
||||
|
||||
await setupNavidrome();
|
||||
async function setupGonic() {
|
||||
console.log("setting up gonic...");
|
||||
|
||||
const baseUrl = "http://gonic";
|
||||
const username = "admin";
|
||||
const password = "admin";
|
||||
|
||||
const client = new SubsonicClient(baseUrl, username, password);
|
||||
await setupTestData(client);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
setupNavidrome(),
|
||||
setupGonic(),
|
||||
]);
|
||||
|
||||
console.log("setup-servers complete");
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
// @deno-types="npm:@types/jsdom@27.0.0"
|
||||
import { JSDOM } from "npm:jsdom@27.1.0";
|
||||
|
||||
export class SubsonicClient {
|
||||
constructor(
|
||||
readonly baseUrl: string,
|
||||
@ -5,7 +8,18 @@ export class SubsonicClient {
|
||||
readonly password: string,
|
||||
) {}
|
||||
|
||||
get(method: string, params?: Record<string, string>) {
|
||||
async get(
|
||||
method: "download",
|
||||
params?: Record<string, string>,
|
||||
): Promise<{ res: Response; xml: undefined }>;
|
||||
async get(
|
||||
method: string,
|
||||
params?: Record<string, string>,
|
||||
): Promise<{ res: Response; xml: Document }>;
|
||||
async get(
|
||||
method: string,
|
||||
params?: Record<string, string>,
|
||||
): Promise<{ res: Response; xml: Document | undefined }> {
|
||||
const url = new URL(`rest/${method}.view`, this.baseUrl);
|
||||
|
||||
url.searchParams.set("u", this.username);
|
||||
@ -19,6 +33,26 @@ export class SubsonicClient {
|
||||
);
|
||||
}
|
||||
|
||||
return fetch(url);
|
||||
const res = await fetch(url);
|
||||
|
||||
let xml: Document | undefined;
|
||||
if (res.headers.get("content-type")?.includes("xml")) {
|
||||
xml = new JSDOM(await res.text(), {
|
||||
contentType: "text/xml",
|
||||
}).window.document;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
let message = `HTTP error ${res.status}`;
|
||||
if (xml) {
|
||||
const error = xml.querySelector("error");
|
||||
const errorCode = error?.getAttribute("code");
|
||||
const errorMessage = error?.getAttribute("message");
|
||||
message += `\nSubsonic error${errorCode}: ${errorMessage}`;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return { res, xml };
|
||||
}
|
||||
}
|
||||
|
||||
3
docker/library-manager/scripts/util/util.ts
Normal file
3
docker/library-manager/scripts/util/util.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function sleep(milliseconds: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'models.freezed.dart';
|
||||
part 'models.g.dart';
|
||||
|
||||
mixin Starred {
|
||||
DateTime? get starred;
|
||||
@ -67,6 +68,9 @@ abstract class SourceItem with _$SourceItem {
|
||||
String? genre,
|
||||
String? coverArt,
|
||||
}) = SourceSong;
|
||||
|
||||
factory SourceItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$SourceItemFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -76,4 +80,7 @@ abstract class SourcePlaylistSong with _$SourcePlaylistSong {
|
||||
required String songId,
|
||||
required int position,
|
||||
}) = _SourcePlaylistSong;
|
||||
|
||||
factory SourcePlaylistSong.fromJson(Map<String, dynamic> json) =>
|
||||
_$SourcePlaylistSongFromJson(json);
|
||||
}
|
||||
|
||||
@ -11,6 +11,38 @@ part of 'models.dart';
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
SourceItem _$SourceItemFromJson(
|
||||
Map<String, dynamic> json
|
||||
) {
|
||||
switch (json['runtimeType']) {
|
||||
case 'artist':
|
||||
return SourceArtist.fromJson(
|
||||
json
|
||||
);
|
||||
case 'album':
|
||||
return SourceAlbum.fromJson(
|
||||
json
|
||||
);
|
||||
case 'playlist':
|
||||
return SourcePlaylist.fromJson(
|
||||
json
|
||||
);
|
||||
case 'song':
|
||||
return SourceSong.fromJson(
|
||||
json
|
||||
);
|
||||
|
||||
default:
|
||||
throw CheckedFromJsonException(
|
||||
json,
|
||||
'runtimeType',
|
||||
'SourceItem',
|
||||
'Invalid union type "${json['runtimeType']}"!'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SourceItem {
|
||||
|
||||
@ -21,6 +53,8 @@ mixin _$SourceItem {
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourceItemCopyWith<SourceItem> get copyWith => _$SourceItemCopyWithImpl<SourceItem>(this as SourceItem, _$identity);
|
||||
|
||||
/// Serializes this SourceItem to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
@ -28,7 +62,7 @@ bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourceItem&&(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id);
|
||||
|
||||
@ -221,11 +255,11 @@ return song(_that.id,_that.albumId,_that.artistId,_that.title,_that.artist,_that
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
class SourceArtist with Starred implements SourceItem {
|
||||
const SourceArtist({required this.id, required this.name, this.starred, this.smallImage, this.largeImage});
|
||||
|
||||
const SourceArtist({required this.id, required this.name, this.starred, this.smallImage, this.largeImage, final String? $type}): $type = $type ?? 'artist';
|
||||
factory SourceArtist.fromJson(Map<String, dynamic> json) => _$SourceArtistFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
final String name;
|
||||
@ -233,20 +267,27 @@ class SourceArtist with Starred implements SourceItem {
|
||||
final Uri? smallImage;
|
||||
final Uri? largeImage;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourceArtistCopyWith<SourceArtist> get copyWith => _$SourceArtistCopyWithImpl<SourceArtist>(this, _$identity);
|
||||
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SourceArtistToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourceArtist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.smallImage, smallImage) || other.smallImage == smallImage)&&(identical(other.largeImage, largeImage) || other.largeImage == largeImage));
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage);
|
||||
|
||||
@ -295,11 +336,11 @@ as Uri?,
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
class SourceAlbum with Starred, CoverArt implements SourceItem {
|
||||
const SourceAlbum({required this.id, this.artistId, required this.name, this.albumArtist, required this.created, this.coverArt, this.year, this.starred, this.genre, this.frequentRank, this.recentRank});
|
||||
|
||||
const SourceAlbum({required this.id, this.artistId, required this.name, this.albumArtist, required this.created, this.coverArt, this.year, this.starred, this.genre, this.frequentRank, this.recentRank, final String? $type}): $type = $type ?? 'album';
|
||||
factory SourceAlbum.fromJson(Map<String, dynamic> json) => _$SourceAlbumFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
final String? artistId;
|
||||
@ -313,20 +354,27 @@ class SourceAlbum with Starred, CoverArt implements SourceItem {
|
||||
final int? frequentRank;
|
||||
final int? recentRank;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourceAlbumCopyWith<SourceAlbum> get copyWith => _$SourceAlbumCopyWithImpl<SourceAlbum>(this, _$identity);
|
||||
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SourceAlbumToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourceAlbum&&(identical(other.id, id) || other.id == id)&&(identical(other.artistId, artistId) || other.artistId == artistId)&&(identical(other.name, name) || other.name == name)&&(identical(other.albumArtist, albumArtist) || other.albumArtist == albumArtist)&&(identical(other.created, created) || other.created == created)&&(identical(other.coverArt, coverArt) || other.coverArt == coverArt)&&(identical(other.year, year) || other.year == year)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.genre, genre) || other.genre == genre)&&(identical(other.frequentRank, frequentRank) || other.frequentRank == frequentRank)&&(identical(other.recentRank, recentRank) || other.recentRank == recentRank));
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,artistId,name,albumArtist,created,coverArt,year,starred,genre,frequentRank,recentRank);
|
||||
|
||||
@ -381,11 +429,11 @@ as int?,
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
class SourcePlaylist with CoverArt implements SourceItem {
|
||||
const SourcePlaylist({required this.id, required this.name, this.comment, required this.created, required this.changed, this.coverArt, this.owner, this.public});
|
||||
|
||||
const SourcePlaylist({required this.id, required this.name, this.comment, required this.created, required this.changed, this.coverArt, this.owner, this.public, final String? $type}): $type = $type ?? 'playlist';
|
||||
factory SourcePlaylist.fromJson(Map<String, dynamic> json) => _$SourcePlaylistFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
final String name;
|
||||
@ -396,20 +444,27 @@ class SourcePlaylist with CoverArt implements SourceItem {
|
||||
final String? owner;
|
||||
final bool? public;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourcePlaylistCopyWith<SourcePlaylist> get copyWith => _$SourcePlaylistCopyWithImpl<SourcePlaylist>(this, _$identity);
|
||||
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SourcePlaylistToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourcePlaylist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.comment, comment) || other.comment == comment)&&(identical(other.created, created) || other.created == created)&&(identical(other.changed, changed) || other.changed == changed)&&(identical(other.coverArt, coverArt) || other.coverArt == coverArt)&&(identical(other.owner, owner) || other.owner == owner)&&(identical(other.public, public) || other.public == public));
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,comment,created,changed,coverArt,owner,public);
|
||||
|
||||
@ -461,11 +516,11 @@ as bool?,
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
class SourceSong with Starred, CoverArt implements SourceItem {
|
||||
const SourceSong({required this.id, this.albumId, this.artistId, required this.title, this.artist, this.album, this.duration, this.track, this.disc, this.starred, this.genre, this.coverArt});
|
||||
|
||||
const SourceSong({required this.id, this.albumId, this.artistId, required this.title, this.artist, this.album, this.duration, this.track, this.disc, this.starred, this.genre, this.coverArt, final String? $type}): $type = $type ?? 'song';
|
||||
factory SourceSong.fromJson(Map<String, dynamic> json) => _$SourceSongFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
final String? albumId;
|
||||
@ -480,20 +535,27 @@ class SourceSong with Starred, CoverArt implements SourceItem {
|
||||
final String? genre;
|
||||
final String? coverArt;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourceSongCopyWith<SourceSong> get copyWith => _$SourceSongCopyWithImpl<SourceSong>(this, _$identity);
|
||||
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SourceSongToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourceSong&&(identical(other.id, id) || other.id == id)&&(identical(other.albumId, albumId) || other.albumId == albumId)&&(identical(other.artistId, artistId) || other.artistId == artistId)&&(identical(other.title, title) || other.title == title)&&(identical(other.artist, artist) || other.artist == artist)&&(identical(other.album, album) || other.album == album)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.track, track) || other.track == track)&&(identical(other.disc, disc) || other.disc == disc)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.genre, genre) || other.genre == genre)&&(identical(other.coverArt, coverArt) || other.coverArt == coverArt));
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,albumId,artistId,title,artist,album,duration,track,disc,starred,genre,coverArt);
|
||||
|
||||
@ -548,6 +610,7 @@ as String?,
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SourcePlaylistSong {
|
||||
|
||||
@ -558,6 +621,8 @@ mixin _$SourcePlaylistSong {
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourcePlaylistSongCopyWith<SourcePlaylistSong> get copyWith => _$SourcePlaylistSongCopyWithImpl<SourcePlaylistSong>(this as SourcePlaylistSong, _$identity);
|
||||
|
||||
/// Serializes this SourcePlaylistSong to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
@ -565,7 +630,7 @@ bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourcePlaylistSong&&(identical(other.playlistId, playlistId) || other.playlistId == playlistId)&&(identical(other.songId, songId) || other.songId == songId)&&(identical(other.position, position) || other.position == position));
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,playlistId,songId,position);
|
||||
|
||||
@ -742,11 +807,11 @@ return $default(_that.playlistId,_that.songId,_that.position);case _:
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
class _SourcePlaylistSong implements SourcePlaylistSong {
|
||||
const _SourcePlaylistSong({required this.playlistId, required this.songId, required this.position});
|
||||
|
||||
factory _SourcePlaylistSong.fromJson(Map<String, dynamic> json) => _$SourcePlaylistSongFromJson(json);
|
||||
|
||||
@override final String playlistId;
|
||||
@override final String songId;
|
||||
@ -758,14 +823,17 @@ class _SourcePlaylistSong implements SourcePlaylistSong {
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SourcePlaylistSongCopyWith<_SourcePlaylistSong> get copyWith => __$SourcePlaylistSongCopyWithImpl<_SourcePlaylistSong>(this, _$identity);
|
||||
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SourcePlaylistSongToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SourcePlaylistSong&&(identical(other.playlistId, playlistId) || other.playlistId == playlistId)&&(identical(other.songId, songId) || other.songId == songId)&&(identical(other.position, position) || other.position == position));
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,playlistId,songId,position);
|
||||
|
||||
|
||||
142
lib/sources/models.g.dart
Normal file
142
lib/sources/models.g.dart
Normal file
@ -0,0 +1,142 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SourceArtist _$SourceArtistFromJson(Map<String, dynamic> json) => SourceArtist(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
starred: json['starred'] == null
|
||||
? null
|
||||
: DateTime.parse(json['starred'] as String),
|
||||
smallImage: json['smallImage'] == null
|
||||
? null
|
||||
: Uri.parse(json['smallImage'] as String),
|
||||
largeImage: json['largeImage'] == null
|
||||
? null
|
||||
: Uri.parse(json['largeImage'] as String),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SourceArtistToJson(SourceArtist instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'starred': instance.starred?.toIso8601String(),
|
||||
'smallImage': instance.smallImage?.toString(),
|
||||
'largeImage': instance.largeImage?.toString(),
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
SourceAlbum _$SourceAlbumFromJson(Map<String, dynamic> json) => SourceAlbum(
|
||||
id: json['id'] as String,
|
||||
artistId: json['artistId'] as String?,
|
||||
name: json['name'] as String,
|
||||
albumArtist: json['albumArtist'] as String?,
|
||||
created: DateTime.parse(json['created'] as String),
|
||||
coverArt: json['coverArt'] as String?,
|
||||
year: (json['year'] as num?)?.toInt(),
|
||||
starred: json['starred'] == null
|
||||
? null
|
||||
: DateTime.parse(json['starred'] as String),
|
||||
genre: json['genre'] as String?,
|
||||
frequentRank: (json['frequentRank'] as num?)?.toInt(),
|
||||
recentRank: (json['recentRank'] as num?)?.toInt(),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SourceAlbumToJson(SourceAlbum instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'artistId': instance.artistId,
|
||||
'name': instance.name,
|
||||
'albumArtist': instance.albumArtist,
|
||||
'created': instance.created.toIso8601String(),
|
||||
'coverArt': instance.coverArt,
|
||||
'year': instance.year,
|
||||
'starred': instance.starred?.toIso8601String(),
|
||||
'genre': instance.genre,
|
||||
'frequentRank': instance.frequentRank,
|
||||
'recentRank': instance.recentRank,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
SourcePlaylist _$SourcePlaylistFromJson(Map<String, dynamic> json) =>
|
||||
SourcePlaylist(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
comment: json['comment'] as String?,
|
||||
created: DateTime.parse(json['created'] as String),
|
||||
changed: DateTime.parse(json['changed'] as String),
|
||||
coverArt: json['coverArt'] as String?,
|
||||
owner: json['owner'] as String?,
|
||||
public: json['public'] as bool?,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SourcePlaylistToJson(SourcePlaylist instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'comment': instance.comment,
|
||||
'created': instance.created.toIso8601String(),
|
||||
'changed': instance.changed.toIso8601String(),
|
||||
'coverArt': instance.coverArt,
|
||||
'owner': instance.owner,
|
||||
'public': instance.public,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
SourceSong _$SourceSongFromJson(Map<String, dynamic> json) => SourceSong(
|
||||
id: json['id'] as String,
|
||||
albumId: json['albumId'] as String?,
|
||||
artistId: json['artistId'] as String?,
|
||||
title: json['title'] as String,
|
||||
artist: json['artist'] as String?,
|
||||
album: json['album'] as String?,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(microseconds: (json['duration'] as num).toInt()),
|
||||
track: (json['track'] as num?)?.toInt(),
|
||||
disc: (json['disc'] as num?)?.toInt(),
|
||||
starred: json['starred'] == null
|
||||
? null
|
||||
: DateTime.parse(json['starred'] as String),
|
||||
genre: json['genre'] as String?,
|
||||
coverArt: json['coverArt'] as String?,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SourceSongToJson(SourceSong instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'albumId': instance.albumId,
|
||||
'artistId': instance.artistId,
|
||||
'title': instance.title,
|
||||
'artist': instance.artist,
|
||||
'album': instance.album,
|
||||
'duration': instance.duration?.inMicroseconds,
|
||||
'track': instance.track,
|
||||
'disc': instance.disc,
|
||||
'starred': instance.starred?.toIso8601String(),
|
||||
'genre': instance.genre,
|
||||
'coverArt': instance.coverArt,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_SourcePlaylistSong _$SourcePlaylistSongFromJson(Map<String, dynamic> json) =>
|
||||
_SourcePlaylistSong(
|
||||
playlistId: json['playlistId'] as String,
|
||||
songId: json['songId'] as String,
|
||||
position: (json['position'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SourcePlaylistSongToJson(_SourcePlaylistSong instance) =>
|
||||
<String, dynamic>{
|
||||
'playlistId': instance.playlistId,
|
||||
'songId': instance.songId,
|
||||
'position': instance.position,
|
||||
};
|
||||
@ -5,12 +5,12 @@ docker compose build
|
||||
docker compose down
|
||||
docker volume rm $(docker compose volumes -q) || true
|
||||
|
||||
docker compose up -d
|
||||
|
||||
docker compose run --rm library-manager music-download.ts
|
||||
docker compose run --rm library-manager setup-servers.ts
|
||||
|
||||
docker compose up -d
|
||||
echo "waiting for library scans..."
|
||||
sleep 10
|
||||
|
||||
docker compose run --rm library-manager setup-servers.ts
|
||||
|
||||
docker compose down
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:subtracks/sources/subsonic/client.dart';
|
||||
import 'package:subtracks/sources/subsonic/source.dart';
|
||||
@ -56,6 +57,33 @@ void main() {
|
||||
final items = await source.allAlbums().toList();
|
||||
|
||||
expect(items.length, equals(3));
|
||||
|
||||
final kosmo = items.firstWhere((a) => a.name == 'Kosmonaut');
|
||||
|
||||
expect(kosmo.id.length, greaterThan(0));
|
||||
expect(kosmo.artistId?.length, greaterThan(0));
|
||||
expect(kosmo.albumArtist, equals('Ugress'));
|
||||
expect(kosmo.created.compareTo(DateTime.now()), lessThan(0));
|
||||
expect(kosmo.coverArt?.length, greaterThan(0));
|
||||
expect(kosmo.year, equals(2006));
|
||||
expect(kosmo.starred, isNull);
|
||||
expect(kosmo.genre, equals('Electronic'));
|
||||
|
||||
final retro = items.firstWhere(
|
||||
(a) => a.name == 'Retroconnaissance EP',
|
||||
);
|
||||
final dunno = items.firstWhere(
|
||||
(a) => a.name == "I Don't Know What I'm Doing",
|
||||
);
|
||||
|
||||
expect(kosmo.recentRank, equals(0));
|
||||
expect(kosmo.frequentRank, equals(1));
|
||||
|
||||
expect(retro.recentRank, equals(1));
|
||||
expect(retro.frequentRank, equals(0));
|
||||
|
||||
expect(dunno.recentRank, isNull);
|
||||
expect(dunno.frequentRank, isNull);
|
||||
});
|
||||
|
||||
test('allArtists', () async {
|
||||
@ -81,6 +109,39 @@ void main() {
|
||||
|
||||
expect(items.length, equals(0));
|
||||
});
|
||||
|
||||
test('album-artist relation', () async {
|
||||
final artists = await source.allArtists().toList();
|
||||
final albums = await source.allAlbums().toList();
|
||||
|
||||
final artistAlbums = artists
|
||||
.map(
|
||||
(artist) => [
|
||||
artist.name,
|
||||
...albums
|
||||
.where((album) => album.artistId == artist.id)
|
||||
.map((album) => album.name)
|
||||
.sorted(),
|
||||
],
|
||||
)
|
||||
.sorted((a, b) => (a[0]).compareTo(b[0]));
|
||||
|
||||
expect(artistAlbums.length, equals(2));
|
||||
expect(
|
||||
artistAlbums,
|
||||
equals([
|
||||
[
|
||||
'Brad Sucks',
|
||||
"I Don't Know What I'm Doing",
|
||||
],
|
||||
[
|
||||
'Ugress',
|
||||
'Kosmonaut',
|
||||
'Retroconnaissance EP',
|
||||
],
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user