mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 17:39:27 +01:00
stop streaming iterables
add gonic to test servers setup gather artist image URLs on allArtists to remove weird Future<Uri> interface for artist images move source options around
This commit is contained in:
parent
3408a3988e
commit
c900c9750a
21
compose.yaml
21
compose.yaml
@ -8,7 +8,7 @@ services:
|
|||||||
navidrome:
|
navidrome:
|
||||||
image: deluan/navidrome:latest
|
image: deluan/navidrome:latest
|
||||||
ports:
|
ports:
|
||||||
- "4533:4533"
|
- 4533:4533
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
ND_LOGLEVEL: debug
|
ND_LOGLEVEL: debug
|
||||||
@ -16,7 +16,26 @@ services:
|
|||||||
- navidrome-data:/data
|
- navidrome-data:/data
|
||||||
- music:/music:ro
|
- music:/music:ro
|
||||||
|
|
||||||
|
gonic:
|
||||||
|
image: sentriz/gonic:latest
|
||||||
|
environment:
|
||||||
|
- TZ
|
||||||
|
- GONIC_SCAN_AT_START_ENABLED=true
|
||||||
|
- GONIC_SCAN_WATCHER_ENABLED=true
|
||||||
|
ports:
|
||||||
|
- 4747:80
|
||||||
|
volumes:
|
||||||
|
- gonic-data:/data
|
||||||
|
- music:/music:ro
|
||||||
|
- gonic-podcasts:/podcasts
|
||||||
|
- gonic-playlists:/playlists
|
||||||
|
- gonic-cache:/cache
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
deno-dir:
|
deno-dir:
|
||||||
music:
|
music:
|
||||||
navidrome-data:
|
navidrome-data:
|
||||||
|
gonic-data:
|
||||||
|
gonic-podcasts:
|
||||||
|
gonic-playlists:
|
||||||
|
gonic-cache:
|
||||||
|
|||||||
@ -19,6 +19,8 @@ abstract class SourceItem with _$SourceItem {
|
|||||||
required String id,
|
required String id,
|
||||||
required String name,
|
required String name,
|
||||||
DateTime? starred,
|
DateTime? starred,
|
||||||
|
Uri? smallImage,
|
||||||
|
Uri? largeImage,
|
||||||
}) = SourceArtist;
|
}) = SourceArtist;
|
||||||
|
|
||||||
@With<Starred>()
|
@With<Starred>()
|
||||||
|
|||||||
@ -159,10 +159,10 @@ return song(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@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;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage)? 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) {
|
switch (_that) {
|
||||||
case SourceArtist() when artist != null:
|
case SourceArtist() when artist != null:
|
||||||
return artist(_that.id,_that.name,_that.starred);case SourceAlbum() when album != null:
|
return artist(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);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 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 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 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 _:
|
||||||
@ -183,10 +183,10 @@ return song(_that.id,_that.albumId,_that.artistId,_that.title,_that.artist,_that
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@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;
|
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage) 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) {
|
switch (_that) {
|
||||||
case SourceArtist():
|
case SourceArtist():
|
||||||
return artist(_that.id,_that.name,_that.starred);case SourceAlbum():
|
return artist(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);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 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 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 _:
|
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 _:
|
||||||
@ -206,10 +206,10 @@ return song(_that.id,_that.albumId,_that.artistId,_that.title,_that.artist,_that
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@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;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage)? 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) {
|
switch (_that) {
|
||||||
case SourceArtist() when artist != null:
|
case SourceArtist() when artist != null:
|
||||||
return artist(_that.id,_that.name,_that.starred);case SourceAlbum() when album != null:
|
return artist(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);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 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 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 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 _:
|
||||||
@ -224,12 +224,14 @@ return song(_that.id,_that.albumId,_that.artistId,_that.title,_that.artist,_that
|
|||||||
|
|
||||||
|
|
||||||
class SourceArtist with Starred implements SourceItem {
|
class SourceArtist with Starred implements SourceItem {
|
||||||
const SourceArtist({required this.id, required this.name, this.starred});
|
const SourceArtist({required this.id, required this.name, this.starred, this.smallImage, this.largeImage});
|
||||||
|
|
||||||
|
|
||||||
@override final String id;
|
@override final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final DateTime? starred;
|
final DateTime? starred;
|
||||||
|
final Uri? smallImage;
|
||||||
|
final Uri? largeImage;
|
||||||
|
|
||||||
/// 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.
|
||||||
@ -241,16 +243,16 @@ $SourceArtistCopyWith<SourceArtist> get copyWith => _$SourceArtistCopyWithImpl<S
|
|||||||
|
|
||||||
@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));
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id,name,starred);
|
int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SourceItem.artist(id: $id, name: $name, starred: $starred)';
|
return 'SourceItem.artist(id: $id, name: $name, starred: $starred, smallImage: $smallImage, largeImage: $largeImage)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -261,7 +263,7 @@ abstract mixin class $SourceArtistCopyWith<$Res> implements $SourceItemCopyWith<
|
|||||||
factory $SourceArtistCopyWith(SourceArtist value, $Res Function(SourceArtist) _then) = _$SourceArtistCopyWithImpl;
|
factory $SourceArtistCopyWith(SourceArtist value, $Res Function(SourceArtist) _then) = _$SourceArtistCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String id, String name, DateTime? starred
|
String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -278,12 +280,14 @@ class _$SourceArtistCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// 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 @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? starred = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? starred = freezed,Object? smallImage = freezed,Object? largeImage = freezed,}) {
|
||||||
return _then(SourceArtist(
|
return _then(SourceArtist(
|
||||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
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,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 String,starred: freezed == starred ? _self.starred : starred // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Uri?,largeImage: freezed == largeImage ? _self.largeImage : largeImage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Uri?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,14 @@ import 'models.dart';
|
|||||||
abstract class MusicSource {
|
abstract class MusicSource {
|
||||||
Future<void> ping();
|
Future<void> ping();
|
||||||
|
|
||||||
Stream<Iterable<SourceAlbum>> allAlbums();
|
Stream<SourceAlbum> allAlbums();
|
||||||
Stream<Iterable<SourceArtist>> allArtists();
|
Stream<SourceArtist> allArtists();
|
||||||
Stream<Iterable<SourcePlaylist>> allPlaylists();
|
Stream<SourcePlaylist> allPlaylists();
|
||||||
Stream<Iterable<SourceSong>> allSongs();
|
Stream<SourceSong> allSongs();
|
||||||
Stream<Iterable<SourcePlaylistSong>> allPlaylistSongs();
|
Stream<SourcePlaylistSong> allPlaylistSongs();
|
||||||
|
|
||||||
Uri streamUri(String songId);
|
Uri streamUri(String songId);
|
||||||
Uri downloadUri(String songId);
|
Uri downloadUri(String songId);
|
||||||
|
|
||||||
Uri coverArtUri(String coverArtId, {bool thumbnail = true});
|
Uri coverArtUri(String coverArtId, {bool thumbnail = true});
|
||||||
Future<Uri?> artistArtUri(String artistId, {bool thumbnail = true});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import 'package:xml/xml.dart';
|
|||||||
|
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
|
||||||
SourceArtist mapArtist(XmlElement e) => SourceArtist(
|
SourceArtist mapArtist(XmlElement e, XmlElement? info) => SourceArtist(
|
||||||
id: e.getAttribute('id')!,
|
id: e.getAttribute('id')!,
|
||||||
name: e.getAttribute('name')!,
|
name: e.getAttribute('name')!,
|
||||||
starred: DateTime.tryParse(e.getAttribute('starred').toString()),
|
starred: DateTime.tryParse(e.getAttribute('starred').toString()),
|
||||||
|
smallImage: Uri.tryParse(info?.getElement('smallImageUrl')?.innerText ?? ''),
|
||||||
|
largeImage: Uri.tryParse(info?.getElement('largeImageUrl')?.innerText ?? ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
SourceAlbum mapAlbum(
|
SourceAlbum mapAlbum(
|
||||||
|
|||||||
@ -9,17 +9,17 @@ import 'client.dart';
|
|||||||
import 'mapping.dart';
|
import 'mapping.dart';
|
||||||
|
|
||||||
class SubsonicSource implements MusicSource {
|
class SubsonicSource implements MusicSource {
|
||||||
SubsonicSource({
|
SubsonicSource(
|
||||||
required this.client,
|
this.client, {
|
||||||
required this.maxBitrate,
|
this.maxConnections = 10,
|
||||||
this.streamFormat,
|
this.connectionPoolTimeout = const Duration(seconds: 60),
|
||||||
});
|
});
|
||||||
|
|
||||||
final SubsonicClient client;
|
final SubsonicClient client;
|
||||||
final int maxBitrate;
|
final int maxConnections;
|
||||||
final String? streamFormat;
|
final Duration connectionPoolTimeout;
|
||||||
|
|
||||||
final _pool = Pool(10, timeout: const Duration(seconds: 60));
|
late final _pool = Pool(maxConnections, timeout: connectionPoolTimeout);
|
||||||
|
|
||||||
bool? _featureEmptyQuerySearch;
|
bool? _featureEmptyQuerySearch;
|
||||||
Future<bool> get supportsFastSongSync async {
|
Future<bool> get supportsFastSongSync async {
|
||||||
@ -41,23 +41,31 @@ class SubsonicSource implements MusicSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Iterable<SourceArtist>> allArtists() async* {
|
Stream<SourceArtist> allArtists() async* {
|
||||||
final res = await client.get('getArtists');
|
final getArtistsRes = await _pool.withResource(
|
||||||
|
() => client.get('getArtists'),
|
||||||
|
);
|
||||||
|
|
||||||
for (var artists in res.xml.findAllElements('artist').slices(200)) {
|
yield* _pool.forEach(getArtistsRes.xml.findAllElements('artist'), (
|
||||||
yield artists.map(mapArtist);
|
artist,
|
||||||
}
|
) async {
|
||||||
|
final res = await client.get('getArtistInfo2', {
|
||||||
|
'id': artist.getAttribute('id')!,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapArtist(artist, res.xml.getElement('artistInfo2'));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Iterable<SourceAlbum>> allAlbums() async* {
|
Stream<SourceAlbum> allAlbums() async* {
|
||||||
final extras = await Future.wait([
|
final extras = await Future.wait([
|
||||||
_albumList(
|
_albumList(
|
||||||
'frequent',
|
'frequent',
|
||||||
).flatten().map((element) => element.getAttribute('id')!).toList(),
|
).flatten().map((e) => e.getAttribute('id')!).toList(),
|
||||||
_albumList(
|
_albumList(
|
||||||
'recent',
|
'recent',
|
||||||
).flatten().map((element) => element.getAttribute('id')!).toList(),
|
).flatten().map((e) => e.getAttribute('id')!).toList(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final frequentlyPlayed = {
|
final frequentlyPlayed = {
|
||||||
@ -67,65 +75,73 @@ class SubsonicSource implements MusicSource {
|
|||||||
for (var i = 0; i < extras[1].length; i++) extras[1][i]: i,
|
for (var i = 0; i < extras[1].length; i++) extras[1][i]: i,
|
||||||
};
|
};
|
||||||
|
|
||||||
await for (var albums in _albumList('newest')) {
|
await for (final albums in _albumList('newest')) {
|
||||||
yield albums.map(
|
yield* Stream.fromIterable(
|
||||||
(e) => mapAlbum(
|
albums.map(
|
||||||
e,
|
(album) => mapAlbum(
|
||||||
frequentRank: frequentlyPlayed[e.getAttribute('id')!],
|
album,
|
||||||
recentRank: recentlyPlayed[e.getAttribute('id')!],
|
frequentRank: frequentlyPlayed[album.getAttribute('id')!],
|
||||||
|
recentRank: recentlyPlayed[album.getAttribute('id')!],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Iterable<SourcePlaylist>> allPlaylists() async* {
|
Stream<SourcePlaylist> allPlaylists() async* {
|
||||||
final res = await client.get('getPlaylists');
|
final res = await _pool.withResource(() => client.get('getPlaylists'));
|
||||||
|
|
||||||
for (var playlists in res.xml.findAllElements('playlist').slices(200)) {
|
yield* Stream.fromIterable(
|
||||||
yield playlists.map(mapPlaylist);
|
res.xml.findAllElements('playlist').map(mapPlaylist),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Iterable<SourcePlaylistSong>> allPlaylistSongs() async* {
|
Stream<SourcePlaylistSong> allPlaylistSongs() async* {
|
||||||
final allPlaylists = await client.get('getPlaylists');
|
final allPlaylists = await _pool.withResource(
|
||||||
|
() => client.get('getPlaylists'),
|
||||||
|
);
|
||||||
|
|
||||||
yield* _pool.forEach(allPlaylists.xml.findAllElements('playlist'), (
|
yield* _pool
|
||||||
|
.forEach(allPlaylists.xml.findAllElements('playlist'), (
|
||||||
playlist,
|
playlist,
|
||||||
) async {
|
) async {
|
||||||
final id = playlist.getAttribute('id')!;
|
final id = playlist.getAttribute('id')!;
|
||||||
final res = await client.get('getPlaylist', {'id': id});
|
final res = await client.get('getPlaylist', {'id': id});
|
||||||
|
|
||||||
return res.xml.findAllElements('entry').mapIndexed(mapPlaylistSong);
|
return res.xml.findAllElements('entry').mapIndexed(mapPlaylistSong);
|
||||||
});
|
})
|
||||||
|
.expand((a) => a);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Iterable<SourceSong>> allSongs() async* {
|
Stream<SourceSong> allSongs() async* {
|
||||||
if (await supportsFastSongSync) {
|
if (await supportsFastSongSync) {
|
||||||
await for (var songs in _songSearch()) {
|
await for (var songs in _songSearch()) {
|
||||||
yield songs.map(mapSong);
|
yield* Stream.fromIterable(songs.map(mapSong));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await for (var albumsList in _albumList('alphabeticalByName')) {
|
await for (var albumsList in _albumList('alphabeticalByName')) {
|
||||||
yield* _pool.forEach(albumsList, (album) async {
|
yield* _pool
|
||||||
|
.forEach(albumsList, (album) async {
|
||||||
final albums = await client.get('getAlbum', {
|
final albums = await client.get('getAlbum', {
|
||||||
'id': album.getAttribute('id')!,
|
'id': album.getAttribute('id')!,
|
||||||
});
|
});
|
||||||
return albums.xml.findAllElements('song').map(mapSong);
|
return albums.xml.findAllElements('song').map(mapSong);
|
||||||
});
|
})
|
||||||
|
.expand((a) => a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Uri streamUri(String songId) {
|
Uri streamUri(String songId, {int? maxBitrate, String? format}) {
|
||||||
return client.uri('stream', {
|
return client.uri('stream', {
|
||||||
'id': songId,
|
'id': songId,
|
||||||
'estimateContentLength': true.toString(),
|
'estimateContentLength': true.toString(),
|
||||||
'maxBitRate': maxBitrate.toString(),
|
'maxBitRate': maxBitrate.toString(),
|
||||||
'format': streamFormat?.toString(),
|
'format': format.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,28 +159,18 @@ class SubsonicSource implements MusicSource {
|
|||||||
return client.uri('getCoverArt', opts);
|
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* {
|
Stream<Iterable<XmlElement>> _albumList(String type) async* {
|
||||||
const size = 500;
|
const size = 500;
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
final res = await client.get('getAlbumList2', {
|
final res = await _pool.withResource(
|
||||||
|
() => client.get('getAlbumList2', {
|
||||||
'type': type,
|
'type': type,
|
||||||
'size': size.toString(),
|
'size': size.toString(),
|
||||||
'offset': offset.toString(),
|
'offset': offset.toString(),
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
final albums = res.xml.findAllElements('album');
|
final albums = res.xml.findAllElements('album');
|
||||||
offset += albums.length;
|
offset += albums.length;
|
||||||
@ -182,13 +188,15 @@ class SubsonicSource implements MusicSource {
|
|||||||
var offset = 0;
|
var offset = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
final res = await client.get('search3', {
|
final res = await _pool.withResource(
|
||||||
|
() => client.get('search3', {
|
||||||
'query': '""',
|
'query': '""',
|
||||||
'songCount': size.toString(),
|
'songCount': size.toString(),
|
||||||
'songOffset': offset.toString(),
|
'songOffset': offset.toString(),
|
||||||
'artistCount': '0',
|
'artistCount': '0',
|
||||||
'albumCount': '0',
|
'albumCount': '0',
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
final songs = res.xml.findAllElements('song');
|
final songs = res.xml.findAllElements('song');
|
||||||
offset += songs.length;
|
offset += songs.length;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
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';
|
||||||
@ -32,12 +31,21 @@ void main() {
|
|||||||
password: 'password',
|
password: 'password',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Server(
|
||||||
|
name: 'gonic',
|
||||||
|
client: SubsonicClient(
|
||||||
|
http: TestHttpClient(),
|
||||||
|
address: Uri.parse('http://localhost:4747/'),
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (final Server(:name, :client) in clients) {
|
for (final Server(:name, :client) in clients) {
|
||||||
group(name, () {
|
group(name, () {
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
source = SubsonicSource(client: client, maxBitrate: 196);
|
source = SubsonicSource(client);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ping', () async {
|
test('ping', () async {
|
||||||
@ -45,22 +53,34 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('allAlbums', () async {
|
test('allAlbums', () async {
|
||||||
final items = (await source.allAlbums().toList()).flattened.toList();
|
final items = await source.allAlbums().toList();
|
||||||
|
|
||||||
expect(items.length, equals(3));
|
expect(items.length, equals(3));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allArtists', () async {
|
test('allArtists', () async {
|
||||||
final items = (await source.allArtists().toList()).flattened.toList();
|
final items = await source.allArtists().toList();
|
||||||
|
|
||||||
expect(items.length, equals(2));
|
expect(items.length, equals(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allSongs', () async {
|
test('allSongs', () async {
|
||||||
final items = (await source.allSongs().toList()).flattened.toList();
|
final items = await source.allSongs().toList();
|
||||||
|
|
||||||
expect(items.length, equals(20));
|
expect(items.length, equals(20));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('allPlaylists', () async {
|
||||||
|
final items = await source.allPlaylists().toList();
|
||||||
|
|
||||||
|
expect(items.length, equals(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allPlaylistSongs', () async {
|
||||||
|
final items = await source.allPlaylistSongs().toList();
|
||||||
|
|
||||||
|
expect(items.length, equals(0));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user