mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
music source and client for subsonic
test fixture setup for navidrome
This commit is contained in:
parent
9f05ebb201
commit
3408a3988e
22
compose.yaml
Normal file
22
compose.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
services:
|
||||
library-manager:
|
||||
build: ./docker/library-manager
|
||||
volumes:
|
||||
- deno-dir:/deno-dir
|
||||
- music:/music
|
||||
|
||||
navidrome:
|
||||
image: deluan/navidrome:latest
|
||||
ports:
|
||||
- "4533:4533"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ND_LOGLEVEL: debug
|
||||
volumes:
|
||||
- navidrome-data:/data
|
||||
- music:/music:ro
|
||||
|
||||
volumes:
|
||||
deno-dir:
|
||||
music:
|
||||
navidrome-data:
|
||||
8
docker/library-manager/Dockerfile
Normal file
8
docker/library-manager/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM denoland/deno:debian
|
||||
|
||||
ENV DENO_DIR=/deno-dir
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y unzip
|
||||
|
||||
COPY scripts /usr/bin
|
||||
44
docker/library-manager/scripts/music-download.ts
Executable file
44
docker/library-manager/scripts/music-download.ts
Executable file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env -S deno --allow-all
|
||||
import * as path from "jsr:@std/path@1.1.2";
|
||||
import { MUSIC_DIR } from "./util/env.ts";
|
||||
import { SubsonicClient } from "./util/subsonic.ts";
|
||||
|
||||
await new Deno.Command("rm", { args: ["-rf", path.join(MUSIC_DIR, "*")] })
|
||||
.output();
|
||||
|
||||
const client = new SubsonicClient(
|
||||
"http://demo.subsonic.org",
|
||||
"guest1",
|
||||
"guest",
|
||||
);
|
||||
|
||||
for (const id of ["197", "199", "321"]) {
|
||||
const res = await client.get("download", { id });
|
||||
|
||||
let filename = res.headers.get("Content-Disposition")
|
||||
?.split(";")[1];
|
||||
filename = (filename?.includes("*=")
|
||||
? decodeURIComponent(filename.split("''")[1])
|
||||
: filename?.split("=")[1]) ?? `${id}.zip`;
|
||||
|
||||
console.log("downloading album:", filename);
|
||||
const downloadPath = path.join(MUSIC_DIR, filename);
|
||||
|
||||
const file = await Deno.open(downloadPath, {
|
||||
write: true,
|
||||
create: true,
|
||||
});
|
||||
await res.body?.pipeTo(file.writable);
|
||||
|
||||
await new Deno.Command("unzip", {
|
||||
args: [
|
||||
downloadPath,
|
||||
"-d",
|
||||
path.join(MUSIC_DIR, filename.split(".")[0]),
|
||||
],
|
||||
}).output();
|
||||
|
||||
await Deno.remove(downloadPath);
|
||||
}
|
||||
|
||||
console.log("music-download complete");
|
||||
18
docker/library-manager/scripts/setup-servers.ts
Executable file
18
docker/library-manager/scripts/setup-servers.ts
Executable file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env -S deno --allow-all
|
||||
|
||||
async function setupNavidrome() {
|
||||
console.log("setting up navidrome...");
|
||||
|
||||
await fetch("http://navidrome:4533/auth/createAdmin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: "admin",
|
||||
password: "password",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await setupNavidrome();
|
||||
|
||||
console.log("setup-servers complete");
|
||||
1
docker/library-manager/scripts/util/env.ts
Normal file
1
docker/library-manager/scripts/util/env.ts
Normal file
@ -0,0 +1 @@
|
||||
export const MUSIC_DIR = Deno.env.get("MUSIC_DIR") ?? "/music";
|
||||
24
docker/library-manager/scripts/util/subsonic.ts
Normal file
24
docker/library-manager/scripts/util/subsonic.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export class SubsonicClient {
|
||||
constructor(
|
||||
readonly baseUrl: string,
|
||||
readonly username: string,
|
||||
readonly password: string,
|
||||
) {}
|
||||
|
||||
get(method: string, params?: Record<string, string>) {
|
||||
const url = new URL(`rest/${method}.view`, this.baseUrl);
|
||||
|
||||
url.searchParams.set("u", this.username);
|
||||
url.searchParams.set("p", this.password);
|
||||
url.searchParams.set("v", "1.13.0");
|
||||
url.searchParams.set("c", "subtracks-test-fixture");
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) =>
|
||||
url.searchParams.append(key, value)
|
||||
);
|
||||
}
|
||||
|
||||
return fetch(url);
|
||||
}
|
||||
}
|
||||
77
lib/sources/models.dart
Normal file
77
lib/sources/models.dart
Normal file
@ -0,0 +1,77 @@
|
||||
// ignore_for_file: annotate_overrides
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'models.freezed.dart';
|
||||
|
||||
mixin Starred {
|
||||
DateTime? get starred;
|
||||
}
|
||||
|
||||
mixin CoverArt {
|
||||
String? get coverArt;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SourceItem with _$SourceItem {
|
||||
@With<Starred>()
|
||||
const factory SourceItem.artist({
|
||||
required String id,
|
||||
required String name,
|
||||
DateTime? starred,
|
||||
}) = SourceArtist;
|
||||
|
||||
@With<Starred>()
|
||||
@With<CoverArt>()
|
||||
const factory SourceItem.album({
|
||||
required String id,
|
||||
String? artistId,
|
||||
required String name,
|
||||
String? albumArtist,
|
||||
required DateTime created,
|
||||
String? coverArt,
|
||||
int? year,
|
||||
DateTime? starred,
|
||||
String? genre,
|
||||
int? frequentRank,
|
||||
int? recentRank,
|
||||
}) = SourceAlbum;
|
||||
|
||||
@With<CoverArt>()
|
||||
const factory SourceItem.playlist({
|
||||
required String id,
|
||||
required String name,
|
||||
String? comment,
|
||||
required DateTime created,
|
||||
required DateTime changed,
|
||||
String? coverArt,
|
||||
String? owner,
|
||||
bool? public,
|
||||
}) = SourcePlaylist;
|
||||
|
||||
@With<Starred>()
|
||||
@With<CoverArt>()
|
||||
const factory SourceItem.song({
|
||||
required String id,
|
||||
String? albumId,
|
||||
String? artistId,
|
||||
required String title,
|
||||
String? artist,
|
||||
String? album,
|
||||
Duration? duration,
|
||||
int? track,
|
||||
int? disc,
|
||||
DateTime? starred,
|
||||
String? genre,
|
||||
String? coverArt,
|
||||
}) = SourceSong;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SourcePlaylistSong with _$SourcePlaylistSong {
|
||||
const factory SourcePlaylistSong({
|
||||
required String playlistId,
|
||||
required String songId,
|
||||
required int position,
|
||||
}) = _SourcePlaylistSong;
|
||||
}
|
||||
810
lib/sources/models.freezed.dart
Normal file
810
lib/sources/models.freezed.dart
Normal file
@ -0,0 +1,810 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$SourceItem {
|
||||
|
||||
String get id;
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourceItemCopyWith<SourceItem> get copyWith => _$SourceItemCopyWithImpl<SourceItem>(this as SourceItem, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SourceItem&&(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourceItem(id: $id)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SourceItemCopyWith<$Res> {
|
||||
factory $SourceItemCopyWith(SourceItem value, $Res Function(SourceItem) _then) = _$SourceItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SourceItemCopyWithImpl<$Res>
|
||||
implements $SourceItemCopyWith<$Res> {
|
||||
_$SourceItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SourceItem _self;
|
||||
final $Res Function(SourceItem) _then;
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SourceItem].
|
||||
extension SourceItemPatterns on SourceItem {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( SourceArtist value)? artist,TResult Function( SourceAlbum value)? album,TResult Function( SourcePlaylist value)? playlist,TResult Function( SourceSong value)? song,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case SourceArtist() when artist != null:
|
||||
return artist(_that);case SourceAlbum() when album != null:
|
||||
return album(_that);case SourcePlaylist() when playlist != null:
|
||||
return playlist(_that);case SourceSong() when song != null:
|
||||
return song(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( SourceArtist value) artist,required TResult Function( SourceAlbum value) album,required TResult Function( SourcePlaylist value) playlist,required TResult Function( SourceSong value) song,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case SourceArtist():
|
||||
return artist(_that);case SourceAlbum():
|
||||
return album(_that);case SourcePlaylist():
|
||||
return playlist(_that);case SourceSong():
|
||||
return song(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( SourceArtist value)? artist,TResult? Function( SourceAlbum value)? album,TResult? Function( SourcePlaylist value)? playlist,TResult? Function( SourceSong value)? song,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case SourceArtist() when artist != null:
|
||||
return artist(_that);case SourceAlbum() when album != null:
|
||||
return album(_that);case SourcePlaylist() when playlist != null:
|
||||
return playlist(_that);case SourceSong() when song != null:
|
||||
return song(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String id, String name, DateTime? starred)? artist,TResult Function( String id, String? artistId, String name, String? albumArtist, DateTime created, String? coverArt, int? year, DateTime? starred, String? genre, int? frequentRank, int? recentRank)? album,TResult Function( String id, String name, String? comment, DateTime created, DateTime changed, String? coverArt, String? owner, bool? public)? playlist,TResult Function( String id, String? albumId, String? artistId, String title, String? artist, String? album, Duration? duration, int? track, int? disc, DateTime? starred, String? genre, String? coverArt)? song,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case SourceArtist() when artist != null:
|
||||
return artist(_that.id,_that.name,_that.starred);case SourceAlbum() when album != null:
|
||||
return album(_that.id,_that.artistId,_that.name,_that.albumArtist,_that.created,_that.coverArt,_that.year,_that.starred,_that.genre,_that.frequentRank,_that.recentRank);case SourcePlaylist() when playlist != null:
|
||||
return playlist(_that.id,_that.name,_that.comment,_that.created,_that.changed,_that.coverArt,_that.owner,_that.public);case SourceSong() when song != null:
|
||||
return song(_that.id,_that.albumId,_that.artistId,_that.title,_that.artist,_that.album,_that.duration,_that.track,_that.disc,_that.starred,_that.genre,_that.coverArt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String id, String name, DateTime? starred) artist,required TResult Function( String id, String? artistId, String name, String? albumArtist, DateTime created, String? coverArt, int? year, DateTime? starred, String? genre, int? frequentRank, int? recentRank) album,required TResult Function( String id, String name, String? comment, DateTime created, DateTime changed, String? coverArt, String? owner, bool? public) playlist,required TResult Function( String id, String? albumId, String? artistId, String title, String? artist, String? album, Duration? duration, int? track, int? disc, DateTime? starred, String? genre, String? coverArt) song,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case SourceArtist():
|
||||
return artist(_that.id,_that.name,_that.starred);case SourceAlbum():
|
||||
return album(_that.id,_that.artistId,_that.name,_that.albumArtist,_that.created,_that.coverArt,_that.year,_that.starred,_that.genre,_that.frequentRank,_that.recentRank);case SourcePlaylist():
|
||||
return playlist(_that.id,_that.name,_that.comment,_that.created,_that.changed,_that.coverArt,_that.owner,_that.public);case SourceSong():
|
||||
return song(_that.id,_that.albumId,_that.artistId,_that.title,_that.artist,_that.album,_that.duration,_that.track,_that.disc,_that.starred,_that.genre,_that.coverArt);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String id, String name, DateTime? starred)? artist,TResult? Function( String id, String? artistId, String name, String? albumArtist, DateTime created, String? coverArt, int? year, DateTime? starred, String? genre, int? frequentRank, int? recentRank)? album,TResult? Function( String id, String name, String? comment, DateTime created, DateTime changed, String? coverArt, String? owner, bool? public)? playlist,TResult? Function( String id, String? albumId, String? artistId, String title, String? artist, String? album, Duration? duration, int? track, int? disc, DateTime? starred, String? genre, String? coverArt)? song,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case SourceArtist() when artist != null:
|
||||
return artist(_that.id,_that.name,_that.starred);case SourceAlbum() when album != null:
|
||||
return album(_that.id,_that.artistId,_that.name,_that.albumArtist,_that.created,_that.coverArt,_that.year,_that.starred,_that.genre,_that.frequentRank,_that.recentRank);case SourcePlaylist() when playlist != null:
|
||||
return playlist(_that.id,_that.name,_that.comment,_that.created,_that.changed,_that.coverArt,_that.owner,_that.public);case SourceSong() when song != null:
|
||||
return song(_that.id,_that.albumId,_that.artistId,_that.title,_that.artist,_that.album,_that.duration,_that.track,_that.disc,_that.starred,_that.genre,_that.coverArt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class SourceArtist with Starred implements SourceItem {
|
||||
const SourceArtist({required this.id, required this.name, this.starred});
|
||||
|
||||
|
||||
@override final String id;
|
||||
final String name;
|
||||
final DateTime? starred;
|
||||
|
||||
/// 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
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,starred);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourceItem.artist(id: $id, name: $name, starred: $starred)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SourceArtistCopyWith<$Res> implements $SourceItemCopyWith<$Res> {
|
||||
factory $SourceArtistCopyWith(SourceArtist value, $Res Function(SourceArtist) _then) = _$SourceArtistCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, DateTime? starred
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SourceArtistCopyWithImpl<$Res>
|
||||
implements $SourceArtistCopyWith<$Res> {
|
||||
_$SourceArtistCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SourceArtist _self;
|
||||
final $Res Function(SourceArtist) _then;
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? starred = freezed,}) {
|
||||
return _then(SourceArtist(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,starred: freezed == starred ? _self.starred : starred // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
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});
|
||||
|
||||
|
||||
@override final String id;
|
||||
final String? artistId;
|
||||
final String name;
|
||||
final String? albumArtist;
|
||||
final DateTime created;
|
||||
final String? coverArt;
|
||||
final int? year;
|
||||
final DateTime? starred;
|
||||
final String? genre;
|
||||
final int? frequentRank;
|
||||
final int? recentRank;
|
||||
|
||||
/// 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
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,artistId,name,albumArtist,created,coverArt,year,starred,genre,frequentRank,recentRank);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourceItem.album(id: $id, artistId: $artistId, name: $name, albumArtist: $albumArtist, created: $created, coverArt: $coverArt, year: $year, starred: $starred, genre: $genre, frequentRank: $frequentRank, recentRank: $recentRank)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SourceAlbumCopyWith<$Res> implements $SourceItemCopyWith<$Res> {
|
||||
factory $SourceAlbumCopyWith(SourceAlbum value, $Res Function(SourceAlbum) _then) = _$SourceAlbumCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String? artistId, String name, String? albumArtist, DateTime created, String? coverArt, int? year, DateTime? starred, String? genre, int? frequentRank, int? recentRank
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SourceAlbumCopyWithImpl<$Res>
|
||||
implements $SourceAlbumCopyWith<$Res> {
|
||||
_$SourceAlbumCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SourceAlbum _self;
|
||||
final $Res Function(SourceAlbum) _then;
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? artistId = freezed,Object? name = null,Object? albumArtist = freezed,Object? created = null,Object? coverArt = freezed,Object? year = freezed,Object? starred = freezed,Object? genre = freezed,Object? frequentRank = freezed,Object? recentRank = freezed,}) {
|
||||
return _then(SourceAlbum(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,artistId: freezed == artistId ? _self.artistId : artistId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,albumArtist: freezed == albumArtist ? _self.albumArtist : albumArtist // ignore: cast_nullable_to_non_nullable
|
||||
as String?,created: null == created ? _self.created : created // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,coverArt: freezed == coverArt ? _self.coverArt : coverArt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,year: freezed == year ? _self.year : year // ignore: cast_nullable_to_non_nullable
|
||||
as int?,starred: freezed == starred ? _self.starred : starred // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,genre: freezed == genre ? _self.genre : genre // ignore: cast_nullable_to_non_nullable
|
||||
as String?,frequentRank: freezed == frequentRank ? _self.frequentRank : frequentRank // ignore: cast_nullable_to_non_nullable
|
||||
as int?,recentRank: freezed == recentRank ? _self.recentRank : recentRank // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
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});
|
||||
|
||||
|
||||
@override final String id;
|
||||
final String name;
|
||||
final String? comment;
|
||||
final DateTime created;
|
||||
final DateTime changed;
|
||||
final String? coverArt;
|
||||
final String? owner;
|
||||
final bool? public;
|
||||
|
||||
/// 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
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,comment,created,changed,coverArt,owner,public);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourceItem.playlist(id: $id, name: $name, comment: $comment, created: $created, changed: $changed, coverArt: $coverArt, owner: $owner, public: $public)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SourcePlaylistCopyWith<$Res> implements $SourceItemCopyWith<$Res> {
|
||||
factory $SourcePlaylistCopyWith(SourcePlaylist value, $Res Function(SourcePlaylist) _then) = _$SourcePlaylistCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String? comment, DateTime created, DateTime changed, String? coverArt, String? owner, bool? public
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SourcePlaylistCopyWithImpl<$Res>
|
||||
implements $SourcePlaylistCopyWith<$Res> {
|
||||
_$SourcePlaylistCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SourcePlaylist _self;
|
||||
final $Res Function(SourcePlaylist) _then;
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? comment = freezed,Object? created = null,Object? changed = null,Object? coverArt = freezed,Object? owner = freezed,Object? public = freezed,}) {
|
||||
return _then(SourcePlaylist(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,comment: freezed == comment ? _self.comment : comment // ignore: cast_nullable_to_non_nullable
|
||||
as String?,created: null == created ? _self.created : created // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,changed: null == changed ? _self.changed : changed // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,coverArt: freezed == coverArt ? _self.coverArt : coverArt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,owner: freezed == owner ? _self.owner : owner // ignore: cast_nullable_to_non_nullable
|
||||
as String?,public: freezed == public ? _self.public : public // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
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});
|
||||
|
||||
|
||||
@override final String id;
|
||||
final String? albumId;
|
||||
final String? artistId;
|
||||
final String title;
|
||||
final String? artist;
|
||||
final String? album;
|
||||
final Duration? duration;
|
||||
final int? track;
|
||||
final int? disc;
|
||||
final DateTime? starred;
|
||||
final String? genre;
|
||||
final String? coverArt;
|
||||
|
||||
/// 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
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,albumId,artistId,title,artist,album,duration,track,disc,starred,genre,coverArt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourceItem.song(id: $id, albumId: $albumId, artistId: $artistId, title: $title, artist: $artist, album: $album, duration: $duration, track: $track, disc: $disc, starred: $starred, genre: $genre, coverArt: $coverArt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SourceSongCopyWith<$Res> implements $SourceItemCopyWith<$Res> {
|
||||
factory $SourceSongCopyWith(SourceSong value, $Res Function(SourceSong) _then) = _$SourceSongCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String? albumId, String? artistId, String title, String? artist, String? album, Duration? duration, int? track, int? disc, DateTime? starred, String? genre, String? coverArt
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SourceSongCopyWithImpl<$Res>
|
||||
implements $SourceSongCopyWith<$Res> {
|
||||
_$SourceSongCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SourceSong _self;
|
||||
final $Res Function(SourceSong) _then;
|
||||
|
||||
/// Create a copy of SourceItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? albumId = freezed,Object? artistId = freezed,Object? title = null,Object? artist = freezed,Object? album = freezed,Object? duration = freezed,Object? track = freezed,Object? disc = freezed,Object? starred = freezed,Object? genre = freezed,Object? coverArt = freezed,}) {
|
||||
return _then(SourceSong(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,albumId: freezed == albumId ? _self.albumId : albumId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,artistId: freezed == artistId ? _self.artistId : artistId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,artist: freezed == artist ? _self.artist : artist // ignore: cast_nullable_to_non_nullable
|
||||
as String?,album: freezed == album ? _self.album : album // ignore: cast_nullable_to_non_nullable
|
||||
as String?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
|
||||
as Duration?,track: freezed == track ? _self.track : track // ignore: cast_nullable_to_non_nullable
|
||||
as int?,disc: freezed == disc ? _self.disc : disc // ignore: cast_nullable_to_non_nullable
|
||||
as int?,starred: freezed == starred ? _self.starred : starred // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,genre: freezed == genre ? _self.genre : genre // ignore: cast_nullable_to_non_nullable
|
||||
as String?,coverArt: freezed == coverArt ? _self.coverArt : coverArt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SourcePlaylistSong {
|
||||
|
||||
String get playlistId; String get songId; int get position;
|
||||
/// Create a copy of SourcePlaylistSong
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SourcePlaylistSongCopyWith<SourcePlaylistSong> get copyWith => _$SourcePlaylistSongCopyWithImpl<SourcePlaylistSong>(this as SourcePlaylistSong, _$identity);
|
||||
|
||||
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,playlistId,songId,position);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourcePlaylistSong(playlistId: $playlistId, songId: $songId, position: $position)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SourcePlaylistSongCopyWith<$Res> {
|
||||
factory $SourcePlaylistSongCopyWith(SourcePlaylistSong value, $Res Function(SourcePlaylistSong) _then) = _$SourcePlaylistSongCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String playlistId, String songId, int position
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SourcePlaylistSongCopyWithImpl<$Res>
|
||||
implements $SourcePlaylistSongCopyWith<$Res> {
|
||||
_$SourcePlaylistSongCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SourcePlaylistSong _self;
|
||||
final $Res Function(SourcePlaylistSong) _then;
|
||||
|
||||
/// Create a copy of SourcePlaylistSong
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? playlistId = null,Object? songId = null,Object? position = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
playlistId: null == playlistId ? _self.playlistId : playlistId // ignore: cast_nullable_to_non_nullable
|
||||
as String,songId: null == songId ? _self.songId : songId // ignore: cast_nullable_to_non_nullable
|
||||
as String,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SourcePlaylistSong].
|
||||
extension SourcePlaylistSongPatterns on SourcePlaylistSong {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SourcePlaylistSong value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SourcePlaylistSong() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SourcePlaylistSong value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SourcePlaylistSong():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SourcePlaylistSong value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SourcePlaylistSong() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String playlistId, String songId, int position)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SourcePlaylistSong() when $default != null:
|
||||
return $default(_that.playlistId,_that.songId,_that.position);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String playlistId, String songId, int position) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SourcePlaylistSong():
|
||||
return $default(_that.playlistId,_that.songId,_that.position);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String playlistId, String songId, int position)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SourcePlaylistSong() when $default != null:
|
||||
return $default(_that.playlistId,_that.songId,_that.position);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _SourcePlaylistSong implements SourcePlaylistSong {
|
||||
const _SourcePlaylistSong({required this.playlistId, required this.songId, required this.position});
|
||||
|
||||
|
||||
@override final String playlistId;
|
||||
@override final String songId;
|
||||
@override final int position;
|
||||
|
||||
/// Create a copy of SourcePlaylistSong
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SourcePlaylistSongCopyWith<_SourcePlaylistSong> get copyWith => __$SourcePlaylistSongCopyWithImpl<_SourcePlaylistSong>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,playlistId,songId,position);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourcePlaylistSong(playlistId: $playlistId, songId: $songId, position: $position)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SourcePlaylistSongCopyWith<$Res> implements $SourcePlaylistSongCopyWith<$Res> {
|
||||
factory _$SourcePlaylistSongCopyWith(_SourcePlaylistSong value, $Res Function(_SourcePlaylistSong) _then) = __$SourcePlaylistSongCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String playlistId, String songId, int position
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SourcePlaylistSongCopyWithImpl<$Res>
|
||||
implements _$SourcePlaylistSongCopyWith<$Res> {
|
||||
__$SourcePlaylistSongCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SourcePlaylistSong _self;
|
||||
final $Res Function(_SourcePlaylistSong) _then;
|
||||
|
||||
/// Create a copy of SourcePlaylistSong
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? playlistId = null,Object? songId = null,Object? position = null,}) {
|
||||
return _then(_SourcePlaylistSong(
|
||||
playlistId: null == playlistId ? _self.playlistId : playlistId // ignore: cast_nullable_to_non_nullable
|
||||
as String,songId: null == songId ? _self.songId : songId // ignore: cast_nullable_to_non_nullable
|
||||
as String,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
17
lib/sources/music_source.dart
Normal file
17
lib/sources/music_source.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'models.dart';
|
||||
|
||||
abstract class MusicSource {
|
||||
Future<void> ping();
|
||||
|
||||
Stream<Iterable<SourceAlbum>> allAlbums();
|
||||
Stream<Iterable<SourceArtist>> allArtists();
|
||||
Stream<Iterable<SourcePlaylist>> allPlaylists();
|
||||
Stream<Iterable<SourceSong>> allSongs();
|
||||
Stream<Iterable<SourcePlaylistSong>> allPlaylistSongs();
|
||||
|
||||
Uri streamUri(String songId);
|
||||
Uri downloadUri(String songId);
|
||||
|
||||
Uri coverArtUri(String coverArtId, {bool thumbnail = true});
|
||||
Future<Uri?> artistArtUri(String artistId, {bool thumbnail = true});
|
||||
}
|
||||
85
lib/sources/subsonic/client.dart
Normal file
85
lib/sources/subsonic/client.dart
Normal file
@ -0,0 +1,85 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import 'xml.dart';
|
||||
|
||||
class SubsonicClient {
|
||||
SubsonicClient({
|
||||
required this.http,
|
||||
required this.address,
|
||||
required this.username,
|
||||
required this.password,
|
||||
this.useTokenAuth = true,
|
||||
});
|
||||
|
||||
final BaseClient http;
|
||||
final Uri address;
|
||||
final String username;
|
||||
final String password;
|
||||
final bool useTokenAuth;
|
||||
|
||||
Uri uri(
|
||||
String method, [
|
||||
Map<String, String?>? extraParams,
|
||||
]) {
|
||||
final pathSegments = [...address.pathSegments, 'rest', '$method.view'];
|
||||
|
||||
final queryParameters = {
|
||||
..._params(),
|
||||
...(extraParams ?? {}),
|
||||
}..removeWhere((_, value) => value == null);
|
||||
|
||||
return Uri(
|
||||
scheme: address.scheme,
|
||||
host: address.host,
|
||||
port: address.hasPort ? address.port : null,
|
||||
pathSegments: pathSegments,
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SubsonicResponse> get(
|
||||
String method, [
|
||||
Map<String, String?>? extraParams,
|
||||
]) async {
|
||||
final res = await http.get(uri(method, extraParams));
|
||||
|
||||
final subsonicResponse = SubsonicResponse(
|
||||
XmlDocument.parse(utf8.decode(res.bodyBytes)),
|
||||
);
|
||||
|
||||
if (subsonicResponse.status == Status.failed) {
|
||||
final error = SubsonicException(subsonicResponse.xml);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return subsonicResponse;
|
||||
}
|
||||
|
||||
String _salt() {
|
||||
final r = Random();
|
||||
return String.fromCharCodes(
|
||||
List.generate(4, (index) => r.nextInt(92) + 33),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _params() {
|
||||
final Map<String, String> p = {};
|
||||
p['v'] = '1.13.0';
|
||||
p['c'] = 'subtracks';
|
||||
p['u'] = username;
|
||||
|
||||
if (useTokenAuth) {
|
||||
p['s'] = _salt();
|
||||
p['t'] = md5.convert(utf8.encode(password + p['s']!)).toString();
|
||||
} else {
|
||||
p['p'] = password;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
65
lib/sources/subsonic/mapping.dart
Normal file
65
lib/sources/subsonic/mapping.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../models.dart';
|
||||
|
||||
SourceArtist mapArtist(XmlElement e) => SourceArtist(
|
||||
id: e.getAttribute('id')!,
|
||||
name: e.getAttribute('name')!,
|
||||
starred: DateTime.tryParse(e.getAttribute('starred').toString()),
|
||||
);
|
||||
|
||||
SourceAlbum mapAlbum(
|
||||
XmlElement e, {
|
||||
int? frequentRank,
|
||||
int? recentRank,
|
||||
}) => SourceAlbum(
|
||||
id: e.getAttribute('id')!,
|
||||
artistId: e.getAttribute('artistId'),
|
||||
name: e.getAttribute('name')!,
|
||||
albumArtist: e.getAttribute('artist'),
|
||||
created: DateTime.parse(e.getAttribute('created')!),
|
||||
coverArt: e.getAttribute('coverArt'),
|
||||
year: int.tryParse(e.getAttribute('year').toString()),
|
||||
starred: DateTime.tryParse(e.getAttribute('starred').toString()),
|
||||
genre: e.getAttribute('genre'),
|
||||
frequentRank: frequentRank,
|
||||
recentRank: recentRank,
|
||||
);
|
||||
|
||||
SourcePlaylist mapPlaylist(XmlElement e) => SourcePlaylist(
|
||||
id: e.getAttribute('id')!,
|
||||
name: e.getAttribute('name')!,
|
||||
comment: e.getAttribute('comment'),
|
||||
coverArt: e.getAttribute('coverArt'),
|
||||
created: DateTime.parse(e.getAttribute('created')!),
|
||||
changed: DateTime.parse(e.getAttribute('changed')!),
|
||||
owner: e.getAttribute('owner'),
|
||||
public: bool.tryParse(e.getAttribute('public').toString()),
|
||||
);
|
||||
|
||||
SourceSong mapSong(XmlElement e) => SourceSong(
|
||||
id: e.getAttribute('id')!,
|
||||
albumId: e.getAttribute('albumId'),
|
||||
artistId: e.getAttribute('artistId'),
|
||||
title: e.getAttribute('title')!,
|
||||
album: e.getAttribute('album'),
|
||||
artist: e.getAttribute('artist'),
|
||||
duration: e.getAttribute('duration') != null
|
||||
? Duration(
|
||||
seconds: int.parse(e.getAttribute('duration').toString()),
|
||||
)
|
||||
: null,
|
||||
track: int.tryParse(e.getAttribute('track').toString()),
|
||||
disc: int.tryParse(e.getAttribute('discNumber').toString()),
|
||||
starred: DateTime.tryParse(e.getAttribute('starred').toString()),
|
||||
genre: e.getAttribute('genre'),
|
||||
);
|
||||
|
||||
SourcePlaylistSong mapPlaylistSong(
|
||||
int index,
|
||||
XmlElement e,
|
||||
) => SourcePlaylistSong(
|
||||
playlistId: e.parentElement!.getAttribute('id')!,
|
||||
songId: e.getAttribute('id')!,
|
||||
position: index,
|
||||
);
|
||||
203
lib/sources/subsonic/source.dart
Normal file
203
lib/sources/subsonic/source.dart
Normal file
@ -0,0 +1,203 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:pool/pool.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
import 'package:xml/xml_events.dart';
|
||||
|
||||
import '../models.dart';
|
||||
import '../music_source.dart';
|
||||
import 'client.dart';
|
||||
import 'mapping.dart';
|
||||
|
||||
class SubsonicSource implements MusicSource {
|
||||
SubsonicSource({
|
||||
required this.client,
|
||||
required this.maxBitrate,
|
||||
this.streamFormat,
|
||||
});
|
||||
|
||||
final SubsonicClient client;
|
||||
final int maxBitrate;
|
||||
final String? streamFormat;
|
||||
|
||||
final _pool = Pool(10, timeout: const Duration(seconds: 60));
|
||||
|
||||
bool? _featureEmptyQuerySearch;
|
||||
Future<bool> get supportsFastSongSync async {
|
||||
if (_featureEmptyQuerySearch == null) {
|
||||
final res = await client.get(
|
||||
'search3',
|
||||
{'query': '""', 'songCount': '1'},
|
||||
);
|
||||
|
||||
_featureEmptyQuerySearch = res.xml.findAllElements('song').isNotEmpty;
|
||||
}
|
||||
|
||||
return _featureEmptyQuerySearch!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> ping() async {
|
||||
await client.get('ping');
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<SourceArtist>> allArtists() async* {
|
||||
final res = await client.get('getArtists');
|
||||
|
||||
for (var artists in res.xml.findAllElements('artist').slices(200)) {
|
||||
yield artists.map(mapArtist);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<SourceAlbum>> allAlbums() async* {
|
||||
final extras = await Future.wait([
|
||||
_albumList(
|
||||
'frequent',
|
||||
).flatten().map((element) => element.getAttribute('id')!).toList(),
|
||||
_albumList(
|
||||
'recent',
|
||||
).flatten().map((element) => element.getAttribute('id')!).toList(),
|
||||
]);
|
||||
|
||||
final frequentlyPlayed = {
|
||||
for (var i = 0; i < extras[0].length; i++) extras[0][i]: i,
|
||||
};
|
||||
final recentlyPlayed = {
|
||||
for (var i = 0; i < extras[1].length; i++) extras[1][i]: i,
|
||||
};
|
||||
|
||||
await for (var albums in _albumList('newest')) {
|
||||
yield albums.map(
|
||||
(e) => mapAlbum(
|
||||
e,
|
||||
frequentRank: frequentlyPlayed[e.getAttribute('id')!],
|
||||
recentRank: recentlyPlayed[e.getAttribute('id')!],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<SourcePlaylist>> allPlaylists() async* {
|
||||
final res = await client.get('getPlaylists');
|
||||
|
||||
for (var playlists in res.xml.findAllElements('playlist').slices(200)) {
|
||||
yield playlists.map(mapPlaylist);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<SourcePlaylistSong>> allPlaylistSongs() async* {
|
||||
final allPlaylists = await client.get('getPlaylists');
|
||||
|
||||
yield* _pool.forEach(allPlaylists.xml.findAllElements('playlist'), (
|
||||
playlist,
|
||||
) async {
|
||||
final id = playlist.getAttribute('id')!;
|
||||
final res = await client.get('getPlaylist', {'id': id});
|
||||
|
||||
return res.xml.findAllElements('entry').mapIndexed(mapPlaylistSong);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<SourceSong>> allSongs() async* {
|
||||
if (await supportsFastSongSync) {
|
||||
await for (var songs in _songSearch()) {
|
||||
yield songs.map(mapSong);
|
||||
}
|
||||
} else {
|
||||
await for (var albumsList in _albumList('alphabeticalByName')) {
|
||||
yield* _pool.forEach(albumsList, (album) async {
|
||||
final albums = await client.get('getAlbum', {
|
||||
'id': album.getAttribute('id')!,
|
||||
});
|
||||
return albums.xml.findAllElements('song').map(mapSong);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Uri streamUri(String songId) {
|
||||
return client.uri('stream', {
|
||||
'id': songId,
|
||||
'estimateContentLength': true.toString(),
|
||||
'maxBitRate': maxBitrate.toString(),
|
||||
'format': streamFormat?.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Uri downloadUri(String songId) {
|
||||
return client.uri('download', {'id': songId});
|
||||
}
|
||||
|
||||
@override
|
||||
Uri coverArtUri(String id, {bool thumbnail = true}) {
|
||||
final opts = {'id': id};
|
||||
if (thumbnail) {
|
||||
opts['size'] = 256.toString();
|
||||
}
|
||||
return client.uri('getCoverArt', opts);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uri?> artistArtUri(String artistId, {bool thumbnail = true}) async {
|
||||
final res = await client.get('getArtistInfo2', {'id': artistId});
|
||||
return Uri.tryParse(
|
||||
res.xml
|
||||
.getElement('artistInfo2')
|
||||
?.getElement(thumbnail ? 'smallImageUrl' : 'largeImageUrl')
|
||||
?.text ??
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
Stream<Iterable<XmlElement>> _albumList(String type) async* {
|
||||
const size = 500;
|
||||
var offset = 0;
|
||||
|
||||
while (true) {
|
||||
final res = await client.get('getAlbumList2', {
|
||||
'type': type,
|
||||
'size': size.toString(),
|
||||
'offset': offset.toString(),
|
||||
});
|
||||
|
||||
final albums = res.xml.findAllElements('album');
|
||||
offset += albums.length;
|
||||
|
||||
yield albums;
|
||||
|
||||
if (albums.length < size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stream<Iterable<XmlElement>> _songSearch() async* {
|
||||
const size = 500;
|
||||
var offset = 0;
|
||||
|
||||
while (true) {
|
||||
final res = await client.get('search3', {
|
||||
'query': '""',
|
||||
'songCount': size.toString(),
|
||||
'songOffset': offset.toString(),
|
||||
'artistCount': '0',
|
||||
'albumCount': '0',
|
||||
});
|
||||
|
||||
final songs = res.xml.findAllElements('song');
|
||||
offset += songs.length;
|
||||
|
||||
yield songs;
|
||||
|
||||
if (songs.length < size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
lib/sources/subsonic/xml.dart
Normal file
40
lib/sources/subsonic/xml.dart
Normal file
@ -0,0 +1,40 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
enum Status {
|
||||
ok('ok'),
|
||||
failed('failed');
|
||||
|
||||
const Status(this.value);
|
||||
final String value;
|
||||
}
|
||||
|
||||
class SubsonicResponse {
|
||||
SubsonicResponse(XmlDocument xml) {
|
||||
this.xml = xml.getElement('subsonic-response')!;
|
||||
status = Status.values.byName(this.xml.getAttribute('status')!);
|
||||
}
|
||||
|
||||
late Status status;
|
||||
late XmlElement xml;
|
||||
}
|
||||
|
||||
class SubsonicException implements Exception {
|
||||
SubsonicException(this.xml) {
|
||||
try {
|
||||
final error = xml.getElement('error')!;
|
||||
code = int.parse(error.getAttribute('code')!);
|
||||
message = error.getAttribute('message')!;
|
||||
} catch (err) {
|
||||
code = -1;
|
||||
message = 'Unknown error.';
|
||||
}
|
||||
}
|
||||
|
||||
final XmlElement xml;
|
||||
|
||||
late int code;
|
||||
late String message;
|
||||
|
||||
@override
|
||||
String toString() => 'SubsonicException [$code]: $message';
|
||||
}
|
||||
16
mise-tasks/servers-reset.sh
Executable file
16
mise-tasks/servers-reset.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
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
|
||||
|
||||
echo "waiting for library scans..."
|
||||
sleep 10
|
||||
|
||||
docker compose down
|
||||
@ -1,5 +1,6 @@
|
||||
[tools]
|
||||
android-sdk = "latest"
|
||||
deno = "2.5.3"
|
||||
flutter = "3.35"
|
||||
java = "17"
|
||||
yq = "latest"
|
||||
|
||||
56
pubspec.lock
56
pubspec.lock
@ -90,7 +90,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
build_runner:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
|
||||
@ -210,7 +210,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.11.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
@ -234,7 +234,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
@ -368,8 +368,16 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||
@ -425,7 +433,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
@ -473,13 +481,21 @@ packages:
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -640,6 +656,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -657,7 +681,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
@ -765,6 +789,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.8"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -894,7 +926,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
|
||||
@ -1005,6 +1037,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
11
pubspec.yaml
11
pubspec.yaml
@ -8,13 +8,20 @@ environment:
|
||||
|
||||
dependencies:
|
||||
cached_network_image: ^3.4.1
|
||||
collection: ^1.19.1
|
||||
crypto: ^3.0.6
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_hooks: ^0.21.3+1
|
||||
freezed_annotation: ^3.1.0
|
||||
go_router: ^16.2.5
|
||||
hooks_riverpod: ^3.0.3
|
||||
http: ^1.5.0
|
||||
infinite_scroll_pagination: ^5.1.1
|
||||
json_annotation: ^4.9.0
|
||||
material_symbols_icons: ^4.2874.0
|
||||
pool: ^1.5.2
|
||||
xml: ^6.6.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -22,6 +29,10 @@ dev_dependencies:
|
||||
flutter_lints: ^6.0.0
|
||||
custom_lint: ^0.8.0
|
||||
riverpod_lint: ^3.0.3
|
||||
build_runner: ^2.7.1
|
||||
freezed: ^3.2.3
|
||||
json_serializable: ^6.11.1
|
||||
test: ^1.26.2
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
66
test/sources/subsonic_test.dart
Normal file
66
test/sources/subsonic_test.dart
Normal file
@ -0,0 +1,66 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:subtracks/sources/subsonic/client.dart';
|
||||
import 'package:subtracks/sources/subsonic/source.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class TestHttpClient extends BaseClient {
|
||||
@override
|
||||
Future<StreamedResponse> send(BaseRequest request) => request.send();
|
||||
}
|
||||
|
||||
class Server {
|
||||
Server({
|
||||
required this.name,
|
||||
required this.client,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final SubsonicClient client;
|
||||
}
|
||||
|
||||
void main() {
|
||||
late SubsonicSource source;
|
||||
|
||||
final clients = [
|
||||
Server(
|
||||
name: 'navidrome',
|
||||
client: SubsonicClient(
|
||||
http: TestHttpClient(),
|
||||
address: Uri.parse('http://localhost:4533/'),
|
||||
username: 'admin',
|
||||
password: 'password',
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
for (final Server(:name, :client) in clients) {
|
||||
group(name, () {
|
||||
setUp(() async {
|
||||
source = SubsonicSource(client: client, maxBitrate: 196);
|
||||
});
|
||||
|
||||
test('ping', () async {
|
||||
await source.ping();
|
||||
});
|
||||
|
||||
test('allAlbums', () async {
|
||||
final items = (await source.allAlbums().toList()).flattened.toList();
|
||||
|
||||
expect(items.length, equals(3));
|
||||
});
|
||||
|
||||
test('allArtists', () async {
|
||||
final items = (await source.allArtists().toList()).flattened.toList();
|
||||
|
||||
expect(items.length, equals(2));
|
||||
});
|
||||
|
||||
test('allSongs', () async {
|
||||
final items = (await source.allSongs().toList()).flattened.toList();
|
||||
|
||||
expect(items.length, equals(20));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user