mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 09:29:29 +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"]) {
|
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")
|
let filename = res.headers.get("Content-Disposition")
|
||||||
?.split(";")[1];
|
?.split(";")[1];
|
||||||
|
|||||||
@ -1,18 +1,65 @@
|
|||||||
#!/usr/bin/env -S deno --allow-all
|
#!/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() {
|
async function setupNavidrome() {
|
||||||
console.log("setting up navidrome...");
|
console.log("setting up navidrome...");
|
||||||
|
|
||||||
|
const baseUrl = "http://navidrome:4533";
|
||||||
|
const username = "admin";
|
||||||
|
const password = "password";
|
||||||
|
|
||||||
await fetch("http://navidrome:4533/auth/createAdmin", {
|
await fetch("http://navidrome:4533/auth/createAdmin", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ username, password }),
|
||||||
username: "admin",
|
|
||||||
password: "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");
|
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 {
|
export class SubsonicClient {
|
||||||
constructor(
|
constructor(
|
||||||
readonly baseUrl: string,
|
readonly baseUrl: string,
|
||||||
@ -5,7 +8,18 @@ export class SubsonicClient {
|
|||||||
readonly password: string,
|
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);
|
const url = new URL(`rest/${method}.view`, this.baseUrl);
|
||||||
|
|
||||||
url.searchParams.set("u", this.username);
|
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';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'models.freezed.dart';
|
part 'models.freezed.dart';
|
||||||
|
part 'models.g.dart';
|
||||||
|
|
||||||
mixin Starred {
|
mixin Starred {
|
||||||
DateTime? get starred;
|
DateTime? get starred;
|
||||||
@ -67,6 +68,9 @@ abstract class SourceItem with _$SourceItem {
|
|||||||
String? genre,
|
String? genre,
|
||||||
String? coverArt,
|
String? coverArt,
|
||||||
}) = SourceSong;
|
}) = SourceSong;
|
||||||
|
|
||||||
|
factory SourceItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SourceItemFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -76,4 +80,7 @@ abstract class SourcePlaylistSong with _$SourcePlaylistSong {
|
|||||||
required String songId,
|
required String songId,
|
||||||
required int position,
|
required int position,
|
||||||
}) = _SourcePlaylistSong;
|
}) = _SourcePlaylistSong;
|
||||||
|
|
||||||
|
factory SourcePlaylistSong.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SourcePlaylistSongFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,38 @@ part of 'models.dart';
|
|||||||
|
|
||||||
// dart format off
|
// dart format off
|
||||||
T _$identity<T>(T value) => value;
|
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
|
/// @nodoc
|
||||||
mixin _$SourceItem {
|
mixin _$SourceItem {
|
||||||
|
|
||||||
@ -21,6 +53,8 @@ mixin _$SourceItem {
|
|||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SourceItemCopyWith<SourceItem> get copyWith => _$SourceItemCopyWithImpl<SourceItem>(this as SourceItem, _$identity);
|
$SourceItemCopyWith<SourceItem> get copyWith => _$SourceItemCopyWithImpl<SourceItem>(this as SourceItem, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SourceItem to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@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));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourceItem&&(identical(other.id, id) || other.id == id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id);
|
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
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
class SourceArtist with Starred implements SourceItem {
|
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;
|
@override final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@ -233,20 +267,27 @@ class SourceArtist with Starred implements SourceItem {
|
|||||||
final Uri? smallImage;
|
final Uri? smallImage;
|
||||||
final Uri? largeImage;
|
final Uri? largeImage;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
|
||||||
/// Create a copy of SourceItem
|
/// Create a copy of SourceItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SourceArtistCopyWith<SourceArtist> get copyWith => _$SourceArtistCopyWithImpl<SourceArtist>(this, _$identity);
|
$SourceArtistCopyWith<SourceArtist> get copyWith => _$SourceArtistCopyWithImpl<SourceArtist>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SourceArtistToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage);
|
int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage);
|
||||||
|
|
||||||
@ -295,11 +336,11 @@ as Uri?,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
class SourceAlbum with Starred, CoverArt implements SourceItem {
|
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;
|
@override final String id;
|
||||||
final String? artistId;
|
final String? artistId;
|
||||||
@ -313,20 +354,27 @@ class SourceAlbum with Starred, CoverArt implements SourceItem {
|
|||||||
final int? frequentRank;
|
final int? frequentRank;
|
||||||
final int? recentRank;
|
final int? recentRank;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
|
||||||
/// Create a copy of SourceItem
|
/// Create a copy of SourceItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SourceAlbumCopyWith<SourceAlbum> get copyWith => _$SourceAlbumCopyWithImpl<SourceAlbum>(this, _$identity);
|
$SourceAlbumCopyWith<SourceAlbum> get copyWith => _$SourceAlbumCopyWithImpl<SourceAlbum>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SourceAlbumToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id,artistId,name,albumArtist,created,coverArt,year,starred,genre,frequentRank,recentRank);
|
int get hashCode => Object.hash(runtimeType,id,artistId,name,albumArtist,created,coverArt,year,starred,genre,frequentRank,recentRank);
|
||||||
|
|
||||||
@ -381,11 +429,11 @@ as int?,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
class SourcePlaylist with CoverArt implements SourceItem {
|
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;
|
@override final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@ -396,20 +444,27 @@ class SourcePlaylist with CoverArt implements SourceItem {
|
|||||||
final String? owner;
|
final String? owner;
|
||||||
final bool? public;
|
final bool? public;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
|
||||||
/// Create a copy of SourceItem
|
/// Create a copy of SourceItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SourcePlaylistCopyWith<SourcePlaylist> get copyWith => _$SourcePlaylistCopyWithImpl<SourcePlaylist>(this, _$identity);
|
$SourcePlaylistCopyWith<SourcePlaylist> get copyWith => _$SourcePlaylistCopyWithImpl<SourcePlaylist>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SourcePlaylistToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id,name,comment,created,changed,coverArt,owner,public);
|
int get hashCode => Object.hash(runtimeType,id,name,comment,created,changed,coverArt,owner,public);
|
||||||
|
|
||||||
@ -461,11 +516,11 @@ as bool?,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
class SourceSong with Starred, CoverArt implements SourceItem {
|
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;
|
@override final String id;
|
||||||
final String? albumId;
|
final String? albumId;
|
||||||
@ -480,20 +535,27 @@ class SourceSong with Starred, CoverArt implements SourceItem {
|
|||||||
final String? genre;
|
final String? genre;
|
||||||
final String? coverArt;
|
final String? coverArt;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
|
||||||
/// Create a copy of SourceItem
|
/// Create a copy of SourceItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SourceSongCopyWith<SourceSong> get copyWith => _$SourceSongCopyWithImpl<SourceSong>(this, _$identity);
|
$SourceSongCopyWith<SourceSong> get copyWith => _$SourceSongCopyWithImpl<SourceSong>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SourceSongToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id,albumId,artistId,title,artist,album,duration,track,disc,starred,genre,coverArt);
|
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
|
/// @nodoc
|
||||||
mixin _$SourcePlaylistSong {
|
mixin _$SourcePlaylistSong {
|
||||||
|
|
||||||
@ -558,6 +621,8 @@ mixin _$SourcePlaylistSong {
|
|||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SourcePlaylistSongCopyWith<SourcePlaylistSong> get copyWith => _$SourcePlaylistSongCopyWithImpl<SourcePlaylistSong>(this as SourcePlaylistSong, _$identity);
|
$SourcePlaylistSongCopyWith<SourcePlaylistSong> get copyWith => _$SourcePlaylistSongCopyWithImpl<SourcePlaylistSong>(this as SourcePlaylistSong, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this SourcePlaylistSong to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,playlistId,songId,position);
|
int get hashCode => Object.hash(runtimeType,playlistId,songId,position);
|
||||||
|
|
||||||
@ -742,11 +807,11 @@ return $default(_that.playlistId,_that.songId,_that.position);case _:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SourcePlaylistSong implements SourcePlaylistSong {
|
class _SourcePlaylistSong implements SourcePlaylistSong {
|
||||||
const _SourcePlaylistSong({required this.playlistId, required this.songId, required this.position});
|
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 playlistId;
|
||||||
@override final String songId;
|
@override final String songId;
|
||||||
@ -758,14 +823,17 @@ class _SourcePlaylistSong implements SourcePlaylistSong {
|
|||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$SourcePlaylistSongCopyWith<_SourcePlaylistSong> get copyWith => __$SourcePlaylistSongCopyWithImpl<_SourcePlaylistSong>(this, _$identity);
|
_$SourcePlaylistSongCopyWith<_SourcePlaylistSong> get copyWith => __$SourcePlaylistSongCopyWithImpl<_SourcePlaylistSong>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$SourcePlaylistSongToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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));
|
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
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,playlistId,songId,position);
|
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 compose down
|
||||||
docker volume rm $(docker compose volumes -q) || true
|
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 music-download.ts
|
||||||
docker compose run --rm library-manager setup-servers.ts
|
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
echo "waiting for library scans..."
|
echo "waiting for library scans..."
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
|
docker compose run --rm library-manager setup-servers.ts
|
||||||
|
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:subtracks/sources/subsonic/client.dart';
|
import 'package:subtracks/sources/subsonic/client.dart';
|
||||||
import 'package:subtracks/sources/subsonic/source.dart';
|
import 'package:subtracks/sources/subsonic/source.dart';
|
||||||
@ -56,6 +57,33 @@ void main() {
|
|||||||
final items = await source.allAlbums().toList();
|
final items = await source.allAlbums().toList();
|
||||||
|
|
||||||
expect(items.length, equals(3));
|
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 {
|
test('allArtists', () async {
|
||||||
@ -81,6 +109,39 @@ void main() {
|
|||||||
|
|
||||||
expect(items.length, equals(0));
|
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