Compare commits

..

No commits in common. "aaab1d1278ed187fa16f11edcab557aa0df7086e" and "ee2a276f2fb70c3d0625ae01e56e4e1c4e0de764" have entirely different histories.

20 changed files with 108 additions and 297 deletions

View File

@ -2,66 +2,32 @@
import { SubsonicClient } from "./util/subsonic.ts"; import { SubsonicClient } from "./util/subsonic.ts";
import { sleep } from "./util/util.ts"; import { sleep } from "./util/util.ts";
async function getArtistId(
client: SubsonicClient,
artist: string,
): Promise<string> {
const { xml } = await client.get("getArtists");
return xml.querySelector(
`artist[name='${artist.replaceAll("'", "\\'")}']`,
)?.id!;
}
async function getAlbumId(
client: SubsonicClient,
album: string,
): Promise<string> {
const { xml } = await client.get("getAlbumList2", [
["type", "newest"],
]);
return xml.querySelector(
`album[name='${album.replaceAll("'", "\\'")}']`,
)?.id!;
}
async function getSongId( async function getSongId(
client: SubsonicClient, client: SubsonicClient,
album: string, album: string,
track: number, track: number,
): Promise<string> { ): Promise<string> {
const albumId = await getAlbumId(client, album); const { xml: albumsXml } = await client.get("getAlbumList2", [
["type", "newest"],
]);
const albumId = albumsXml.querySelector(
`album[name='${album.replaceAll("'", "\\'")}']`,
)?.id;
const { xml } = await client.get("getAlbum", [["id", albumId!]]); const { xml: songsXml } = await client.get("getAlbum", [["id", albumId!]]);
return xml.querySelector(`song[track='${track}']`)?.id!; return songsXml.querySelector(`song[track='${track}']`)?.id!;
} }
async function scrobbleTrack( async function scrobbleTrack(
client: SubsonicClient, client: SubsonicClient,
album: string, songId: string,
track: number,
) { ) {
const songId = await getSongId(client, album, track);
await client.get("scrobble", [ await client.get("scrobble", [
["id", songId!], ["id", songId!],
["submission", "true"], ["submission", "true"],
]); ]);
} }
async function starAlbum(client: SubsonicClient, album: string) {
const albumId = await getAlbumId(client, album);
await client.get("star", [["albumId", albumId]]);
}
async function starArtist(client: SubsonicClient, artist: string) {
const artistId = await getArtistId(client, artist);
await client.get("star", [["artistId", artistId]]);
}
async function createPlaylist( async function createPlaylist(
client: SubsonicClient, client: SubsonicClient,
name: string, name: string,
@ -78,11 +44,17 @@ async function createPlaylist(
} }
async function setupTestData(client: SubsonicClient) { async function setupTestData(client: SubsonicClient) {
await scrobbleTrack(client, "Retroconnaissance EP", 1); await scrobbleTrack(
client,
await getSongId(client, "Retroconnaissance EP", 1),
);
await sleep(1_000); await sleep(1_000);
await scrobbleTrack(client, "Retroconnaissance EP", 2); await scrobbleTrack(
client,
await getSongId(client, "Retroconnaissance EP", 2),
);
await sleep(1_000); await sleep(1_000);
await scrobbleTrack(client, "Kosmonaut", 1); await scrobbleTrack(client, await getSongId(client, "Kosmonaut", 1));
await createPlaylist(client, "Playlist 1", [ await createPlaylist(client, "Playlist 1", [
{ album: "Retroconnaissance EP", track: 2 }, { album: "Retroconnaissance EP", track: 2 },
@ -93,9 +65,6 @@ async function setupTestData(client: SubsonicClient) {
{ album: "I Don't Know What I'm Doing", track: 10 }, { album: "I Don't Know What I'm Doing", track: 10 },
{ album: "I Don't Know What I'm Doing", track: 11 }, { album: "I Don't Know What I'm Doing", track: 11 },
]); ]);
await starAlbum(client, "Kosmonaut");
await starArtist(client, "Ugress");
} }
async function setupNavidrome() { async function setupNavidrome() {

View File

@ -10,7 +10,7 @@ import '../state/database.dart';
import '../state/settings.dart'; import '../state/settings.dart';
import 'list_items.dart'; import 'list_items.dart';
const kPageSize = 60; const kPageSize = 30;
class AlbumsGrid extends HookConsumerWidget { class AlbumsGrid extends HookConsumerWidget {
const AlbumsGrid({super.key}); const AlbumsGrid({super.key});

View File

@ -1,81 +0,0 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../../sources/models.dart';
import '../hooks/use_paging_controller.dart';
import '../state/database.dart';
import '../state/settings.dart';
import 'list_items.dart';
const kPageSize = 30;
typedef _ArtistItem = ({Artist artist, int? albumCount});
class ArtistsList extends HookConsumerWidget {
const ArtistsList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final db = ref.watch(databaseProvider);
final sourceId = ref.watch(sourceIdProvider);
final controller = usePagingController<int, _ArtistItem>(
getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) async {
final albumCount = db.albums.id.count();
final query =
db.artists.select().join([
leftOuterJoin(
db.albums,
db.albums.artistId.equalsExp(db.artists.id),
),
])
..addColumns([albumCount])
..where(
db.artists.sourceId.equals(sourceId) &
db.albums.sourceId.equals(sourceId),
)
..groupBy([db.artists.sourceId, db.artists.id])
..orderBy([OrderingTerm.asc(db.artists.name)])
..limit(kPageSize, offset: (pageKey - 1) * kPageSize);
return (await query.get())
.map(
(row) => (
artist: row.readTable(db.artists),
albumCount: row.read(albumCount),
),
)
.toList();
},
);
return PagingListener(
controller: controller,
builder: (context, state, fetchNextPage) {
return PagedSliverList(
state: state,
fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate<_ArtistItem>(
itemBuilder: (context, item, index) {
final (:artist, :albumCount) = item;
return ArtistListTile(
artist: artist,
albumCount: albumCount,
onTap: () async {
context.push('/artist/${artist.id}');
},
);
},
),
);
},
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -32,26 +33,19 @@ class AlbumGridTile extends HookConsumerWidget {
} }
class ArtistListTile extends StatelessWidget { class ArtistListTile extends StatelessWidget {
const ArtistListTile({ const ArtistListTile({super.key});
super.key,
required this.artist,
this.albumCount,
this.onTap,
});
final Artist artist;
final int? albumCount;
final void Function()? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
leading: CircleClip( leading: CircleClip(
child: CoverArtImage(coverArt: artist.coverArt), child: CachedNetworkImage(
imageUrl: 'https://placehold.net/400x400.png',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
), ),
title: Text(artist.name), ),
subtitle: albumCount != null ? Text('$albumCount albums') : null, title: Text('Some Artist'),
onTap: onTap,
); );
} }
} }

View File

@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../lists/artists_list.dart'; import '../lists/albums_grid.dart';
import '../state/services.dart'; import '../state/services.dart';
import '../util/custom_scroll_fix.dart'; import '../util/custom_scroll_fix.dart';
@ -210,7 +210,7 @@ class _NewWidgetState extends State<NewWidget>
), ),
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
sliver: ArtistsList(), sliver: AlbumsGrid(),
), ),
], ],
); );

View File

@ -1,6 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../services/sync_service.dart'; import '../../services/sync_services.dart';
import 'database.dart'; import 'database.dart';
import 'settings.dart'; import 'settings.dart';
import 'source.dart'; import 'source.dart';

View File

@ -119,14 +119,14 @@ class CustomScrollController extends ScrollController {
ScrollContext context, ScrollContext context,
ScrollPosition? oldPosition, ScrollPosition? oldPosition,
) { ) {
// debugPrint('$debugLabel-createScrollPosition: $isActive'); debugPrint('$debugLabel-createScrollPosition: $isActive');
return parent.createScrollPosition(physics, context, oldPosition); return parent.createScrollPosition(physics, context, oldPosition);
} }
@override @override
void attach(ScrollPosition position) { void attach(ScrollPosition position) {
// debugPrint('$debugLabel-attach: $isActive'); debugPrint('$debugLabel-attach: $isActive');
super.attach(position); super.attach(position);
if (isActive && !parent.positions.contains(position)) { if (isActive && !parent.positions.contains(position)) {
@ -136,7 +136,7 @@ class CustomScrollController extends ScrollController {
@override @override
void detach(ScrollPosition position) { void detach(ScrollPosition position) {
// debugPrint('$debugLabel-detach: $isActive'); debugPrint('$debugLabel-detach: $isActive');
if (parent.positions.contains(position)) { if (parent.positions.contains(position)) {
parent.detach(position); parent.detach(position);
@ -146,7 +146,7 @@ class CustomScrollController extends ScrollController {
} }
void forceDetach() { void forceDetach() {
// debugPrint('$debugLabel-forceDetach: $isActive'); debugPrint('$debugLabel-forceDetach: $isActive');
for (final position in positions) { for (final position in positions) {
if (parent.positions.contains(position)) { if (parent.positions.contains(position)) {
@ -156,7 +156,7 @@ class CustomScrollController extends ScrollController {
} }
void forceAttach() { void forceAttach() {
// debugPrint('$debugLabel-forceAttach: $isActive'); debugPrint('$debugLabel-forceAttach: $isActive');
for (final position in positions) { for (final position in positions) {
if (!parent.positions.contains(position)) { if (!parent.positions.contains(position)) {
@ -167,7 +167,7 @@ class CustomScrollController extends ScrollController {
@override @override
void dispose() { void dispose() {
// debugPrint('$debugLabel-dispose: $isActive'); debugPrint('$debugLabel-dispose: $isActive');
forceDetach(); forceDetach();
super.dispose(); super.dispose();

View File

@ -437,7 +437,6 @@ extension ArtistToDb on models.Artist {
id: id, id: id,
name: name, name: name,
starred: Value(starred), starred: Value(starred),
coverArt: Value(coverArt),
); );
} }

View File

@ -708,19 +708,8 @@ class Artists extends Table with TableInfo<Artists, models.Artist> {
requiredDuringInsert: false, requiredDuringInsert: false,
$customConstraints: '', $customConstraints: '',
); );
static const VerificationMeta _coverArtMeta = const VerificationMeta(
'coverArt',
);
late final GeneratedColumn<String> coverArt = GeneratedColumn<String>(
'cover_art',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '',
);
@override @override
List<GeneratedColumn> get $columns => [sourceId, id, name, starred, coverArt]; List<GeneratedColumn> get $columns => [sourceId, id, name, starred];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@ -760,12 +749,6 @@ class Artists extends Table with TableInfo<Artists, models.Artist> {
starred.isAcceptableOrUnknown(data['starred']!, _starredMeta), starred.isAcceptableOrUnknown(data['starred']!, _starredMeta),
); );
} }
if (data.containsKey('cover_art')) {
context.handle(
_coverArtMeta,
coverArt.isAcceptableOrUnknown(data['cover_art']!, _coverArtMeta),
);
}
return context; return context;
} }
@ -787,10 +770,6 @@ class Artists extends Table with TableInfo<Artists, models.Artist> {
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}starred'], data['${effectivePrefix}starred'],
), ),
coverArt: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}cover_art'],
),
); );
} }
@ -813,14 +792,12 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
final Value<String> id; final Value<String> id;
final Value<String> name; final Value<String> name;
final Value<DateTime?> starred; final Value<DateTime?> starred;
final Value<String?> coverArt;
final Value<int> rowid; final Value<int> rowid;
const ArtistsCompanion({ const ArtistsCompanion({
this.sourceId = const Value.absent(), this.sourceId = const Value.absent(),
this.id = const Value.absent(), this.id = const Value.absent(),
this.name = const Value.absent(), this.name = const Value.absent(),
this.starred = const Value.absent(), this.starred = const Value.absent(),
this.coverArt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
ArtistsCompanion.insert({ ArtistsCompanion.insert({
@ -828,7 +805,6 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
required String id, required String id,
required String name, required String name,
this.starred = const Value.absent(), this.starred = const Value.absent(),
this.coverArt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : sourceId = Value(sourceId), }) : sourceId = Value(sourceId),
id = Value(id), id = Value(id),
@ -838,7 +814,6 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
Expression<String>? id, Expression<String>? id,
Expression<String>? name, Expression<String>? name,
Expression<DateTime>? starred, Expression<DateTime>? starred,
Expression<String>? coverArt,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
@ -846,7 +821,6 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
if (id != null) 'id': id, if (id != null) 'id': id,
if (name != null) 'name': name, if (name != null) 'name': name,
if (starred != null) 'starred': starred, if (starred != null) 'starred': starred,
if (coverArt != null) 'cover_art': coverArt,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@ -856,7 +830,6 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
Value<String>? id, Value<String>? id,
Value<String>? name, Value<String>? name,
Value<DateTime?>? starred, Value<DateTime?>? starred,
Value<String?>? coverArt,
Value<int>? rowid, Value<int>? rowid,
}) { }) {
return ArtistsCompanion( return ArtistsCompanion(
@ -864,7 +837,6 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
starred: starred ?? this.starred, starred: starred ?? this.starred,
coverArt: coverArt ?? this.coverArt,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@ -884,9 +856,6 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
if (starred.present) { if (starred.present) {
map['starred'] = Variable<DateTime>(starred.value); map['starred'] = Variable<DateTime>(starred.value);
} }
if (coverArt.present) {
map['cover_art'] = Variable<String>(coverArt.value);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@ -900,7 +869,6 @@ class ArtistsCompanion extends UpdateCompanion<models.Artist> {
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')
..write('starred: $starred, ') ..write('starred: $starred, ')
..write('coverArt: $coverArt, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@ -2896,7 +2864,6 @@ typedef $ArtistsCreateCompanionBuilder =
required String id, required String id,
required String name, required String name,
Value<DateTime?> starred, Value<DateTime?> starred,
Value<String?> coverArt,
Value<int> rowid, Value<int> rowid,
}); });
typedef $ArtistsUpdateCompanionBuilder = typedef $ArtistsUpdateCompanionBuilder =
@ -2905,7 +2872,6 @@ typedef $ArtistsUpdateCompanionBuilder =
Value<String> id, Value<String> id,
Value<String> name, Value<String> name,
Value<DateTime?> starred, Value<DateTime?> starred,
Value<String?> coverArt,
Value<int> rowid, Value<int> rowid,
}); });
@ -2936,11 +2902,6 @@ class $ArtistsFilterComposer extends Composer<_$SubtracksDatabase, Artists> {
column: $table.starred, column: $table.starred,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get coverArt => $composableBuilder(
column: $table.coverArt,
builder: (column) => ColumnFilters(column),
);
} }
class $ArtistsOrderingComposer extends Composer<_$SubtracksDatabase, Artists> { class $ArtistsOrderingComposer extends Composer<_$SubtracksDatabase, Artists> {
@ -2970,11 +2931,6 @@ class $ArtistsOrderingComposer extends Composer<_$SubtracksDatabase, Artists> {
column: $table.starred, column: $table.starred,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get coverArt => $composableBuilder(
column: $table.coverArt,
builder: (column) => ColumnOrderings(column),
);
} }
class $ArtistsAnnotationComposer class $ArtistsAnnotationComposer
@ -2997,9 +2953,6 @@ class $ArtistsAnnotationComposer
GeneratedColumn<DateTime> get starred => GeneratedColumn<DateTime> get starred =>
$composableBuilder(column: $table.starred, builder: (column) => column); $composableBuilder(column: $table.starred, builder: (column) => column);
GeneratedColumn<String> get coverArt =>
$composableBuilder(column: $table.coverArt, builder: (column) => column);
} }
class $ArtistsTableManager class $ArtistsTableManager
@ -3037,14 +2990,12 @@ class $ArtistsTableManager
Value<String> id = const Value.absent(), Value<String> id = const Value.absent(),
Value<String> name = const Value.absent(), Value<String> name = const Value.absent(),
Value<DateTime?> starred = const Value.absent(), Value<DateTime?> starred = const Value.absent(),
Value<String?> coverArt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => ArtistsCompanion( }) => ArtistsCompanion(
sourceId: sourceId, sourceId: sourceId,
id: id, id: id,
name: name, name: name,
starred: starred, starred: starred,
coverArt: coverArt,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: createCompanionCallback:
@ -3053,14 +3004,12 @@ class $ArtistsTableManager
required String id, required String id,
required String name, required String name,
Value<DateTime?> starred = const Value.absent(), Value<DateTime?> starred = const Value.absent(),
Value<String?> coverArt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => ArtistsCompanion.insert( }) => ArtistsCompanion.insert(
sourceId: sourceId, sourceId: sourceId,
id: id, id: id,
name: name, name: name,
starred: starred, starred: starred,
coverArt: coverArt,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0

View File

@ -65,7 +65,6 @@ CREATE TABLE artists(
id TEXT NOT NULL, id TEXT NOT NULL,
name TEXT NOT NULL COLLATE NOCASE, name TEXT NOT NULL COLLATE NOCASE,
starred DATETIME, starred DATETIME,
cover_art TEXT,
PRIMARY KEY (source_id, id), PRIMARY KEY (source_id, id),
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
) WITH Artist; ) WITH Artist;

View File

@ -1,9 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:octo_image/octo_image.dart';
import '../app/state/settings.dart';
import '../app/state/source.dart'; import '../app/state/source.dart';
class CoverArtImage extends HookConsumerWidget { class CoverArtImage extends HookConsumerWidget {
@ -19,39 +20,30 @@ class CoverArtImage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final source = ref.watch(sourceProvider); final source = ref.watch(sourceProvider);
final sourceId = ref.watch(sourceIdProvider);
final imageUrl = coverArt != null final imageProviderKeys = [source, coverArt, thumbnail];
final buildImageProvider = useCallback(
() => CachedNetworkImageProvider(
coverArt != null
? source.coverArtUri(coverArt!, thumbnail: thumbnail).toString() ? source.coverArtUri(coverArt!, thumbnail: thumbnail).toString()
: 'https://placehold.net/400x400.png'; : 'https://placehold.net/400x400.png',
),
return BaseImage( imageProviderKeys,
imageUrl: imageUrl,
// can't use the URL because of token auth, which is a cache-buster
cacheKey: '$sourceId$coverArt$thumbnail',
); );
}
}
class BaseImage extends HookConsumerWidget { final imageProvider = useState(buildImageProvider());
const BaseImage({ useEffect(
super.key, () {
required this.imageUrl, imageProvider.value = buildImageProvider();
this.cacheKey, return;
this.fit = BoxFit.cover, },
}); imageProviderKeys,
);
final String imageUrl; return OctoImage(
final String? cacheKey; image: imageProvider.value,
final BoxFit fit; placeholderBuilder: (context) => Icon(Symbols.album_rounded),
errorBuilder: (context, error, trace) => Icon(Icons.error),
@override
Widget build(BuildContext context, WidgetRef ref) {
return CachedNetworkImage(
imageUrl: imageUrl,
cacheKey: cacheKey,
placeholder: (context, url) => Icon(Symbols.cached_rounded),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover, fit: BoxFit.cover,
fadeOutDuration: Duration(milliseconds: 100), fadeOutDuration: Duration(milliseconds: 100),
fadeInDuration: Duration(milliseconds: 200), fadeInDuration: Duration(milliseconds: 200),

View File

@ -22,12 +22,9 @@ void main() async {
.insertOnConflictUpdate( .insertOnConflictUpdate(
SubsonicSettingsCompanion.insert( SubsonicSettingsCompanion.insert(
sourceId: Value(1), sourceId: Value(1),
address: Uri.parse('http://demo.subsonic.org'), address: Uri.parse('http://10.0.2.2:4533'),
username: 'guest1', username: 'admin',
password: 'guest', password: 'password',
// address: Uri.parse('http://10.0.2.2:4533'),
// username: 'admin',
// password: 'password',
useTokenAuth: Value(true), useTokenAuth: Value(true),
), ),
); );

View File

@ -8,7 +8,8 @@ abstract class Artist with _$Artist {
required String id, required String id,
required String name, required String name,
DateTime? starred, DateTime? starred,
String? coverArt, Uri? smallImage,
Uri? largeImage,
}) = _Artist; }) = _Artist;
} }

View File

@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$Artist { mixin _$Artist {
String get id; String get name; DateTime? get starred; String? get coverArt; String get id; String get name; DateTime? get starred; Uri? get smallImage; Uri? get largeImage;
/// Create a copy of Artist /// Create a copy of Artist
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -25,16 +25,16 @@ $ArtistCopyWith<Artist> get copyWith => _$ArtistCopyWithImpl<Artist>(this as Art
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Artist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.coverArt, coverArt) || other.coverArt == coverArt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is Artist&&(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,coverArt); int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage);
@override @override
String toString() { String toString() {
return 'Artist(id: $id, name: $name, starred: $starred, coverArt: $coverArt)'; return 'Artist(id: $id, name: $name, starred: $starred, smallImage: $smallImage, largeImage: $largeImage)';
} }
@ -45,7 +45,7 @@ abstract mixin class $ArtistCopyWith<$Res> {
factory $ArtistCopyWith(Artist value, $Res Function(Artist) _then) = _$ArtistCopyWithImpl; factory $ArtistCopyWith(Artist value, $Res Function(Artist) _then) = _$ArtistCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String name, DateTime? starred, String? coverArt String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage
}); });
@ -62,13 +62,14 @@ class _$ArtistCopyWithImpl<$Res>
/// Create a copy of Artist /// Create a copy of Artist
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? starred = freezed,Object? coverArt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? starred = freezed,Object? smallImage = freezed,Object? largeImage = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
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?,coverArt: freezed == coverArt ? _self.coverArt : coverArt // ignore: cast_nullable_to_non_nullable as DateTime?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable
as String?, as Uri?,largeImage: freezed == largeImage ? _self.largeImage : largeImage // ignore: cast_nullable_to_non_nullable
as Uri?,
)); ));
} }
@ -153,10 +154,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, DateTime? starred, String? coverArt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _Artist() when $default != null: case _Artist() when $default != null:
return $default(_that.id,_that.name,_that.starred,_that.coverArt);case _: return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);case _:
return orElse(); return orElse();
} }
@ -174,10 +175,10 @@ return $default(_that.id,_that.name,_that.starred,_that.coverArt);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, DateTime? starred, String? coverArt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _Artist(): case _Artist():
return $default(_that.id,_that.name,_that.starred,_that.coverArt);case _: return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@ -194,10 +195,10 @@ return $default(_that.id,_that.name,_that.starred,_that.coverArt);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, DateTime? starred, String? coverArt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _Artist() when $default != null: case _Artist() when $default != null:
return $default(_that.id,_that.name,_that.starred,_that.coverArt);case _: return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);case _:
return null; return null;
} }
@ -209,13 +210,14 @@ return $default(_that.id,_that.name,_that.starred,_that.coverArt);case _:
class _Artist implements Artist { class _Artist implements Artist {
const _Artist({required this.id, required this.name, this.starred, this.coverArt}); const _Artist({required this.id, required this.name, this.starred, this.smallImage, this.largeImage});
@override final String id; @override final String id;
@override final String name; @override final String name;
@override final DateTime? starred; @override final DateTime? starred;
@override final String? coverArt; @override final Uri? smallImage;
@override final Uri? largeImage;
/// Create a copy of Artist /// Create a copy of Artist
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -227,16 +229,16 @@ _$ArtistCopyWith<_Artist> get copyWith => __$ArtistCopyWithImpl<_Artist>(this, _
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Artist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.coverArt, coverArt) || other.coverArt == coverArt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _Artist&&(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,coverArt); int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage);
@override @override
String toString() { String toString() {
return 'Artist(id: $id, name: $name, starred: $starred, coverArt: $coverArt)'; return 'Artist(id: $id, name: $name, starred: $starred, smallImage: $smallImage, largeImage: $largeImage)';
} }
@ -247,7 +249,7 @@ abstract mixin class _$ArtistCopyWith<$Res> implements $ArtistCopyWith<$Res> {
factory _$ArtistCopyWith(_Artist value, $Res Function(_Artist) _then) = __$ArtistCopyWithImpl; factory _$ArtistCopyWith(_Artist value, $Res Function(_Artist) _then) = __$ArtistCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String name, DateTime? starred, String? coverArt String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage
}); });
@ -264,13 +266,14 @@ class __$ArtistCopyWithImpl<$Res>
/// Create a copy of Artist /// Create a copy of Artist
/// 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,Object? coverArt = 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(_Artist( return _then(_Artist(
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?,coverArt: freezed == coverArt ? _self.coverArt : coverArt // ignore: cast_nullable_to_non_nullable as DateTime?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable
as String?, as Uri?,largeImage: freezed == largeImage ? _self.largeImage : largeImage // ignore: cast_nullable_to_non_nullable
as Uri?,
)); ));
} }

View File

@ -2,11 +2,12 @@ import 'package:xml/xml.dart';
import '../models.dart'; import '../models.dart';
Artist mapArtist(XmlElement e) => Artist( Artist mapArtist(XmlElement e, XmlElement? info) => Artist(
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()),
coverArt: e.getAttribute('coverArt'), smallImage: Uri.tryParse(info?.getElement('smallImageUrl')?.innerText ?? ''),
largeImage: Uri.tryParse(info?.getElement('largeImageUrl')?.innerText ?? ''),
); );
Album mapAlbum( Album mapAlbum(

View File

@ -42,13 +42,19 @@ class SubsonicSource implements MusicSource {
@override @override
Stream<Artist> allArtists() async* { Stream<Artist> allArtists() async* {
final res = await _pool.withResource( final getArtistsRes = await _pool.withResource(
() => client.get('getArtists'), () => client.get('getArtists'),
); );
yield* Stream.fromIterable( yield* _pool.forEach(getArtistsRes.xml.findAllElements('artist'), (
res.xml.findAllElements('artist').map(mapArtist), artist,
); ) async {
final res = await client.get('getArtistInfo2', {
'id': artist.getAttribute('id')!,
});
return mapArtist(artist, res.xml.getElement('artistInfo2'));
});
} }
@override @override

View File

@ -1,5 +1,5 @@
import 'package:subtracks/database/database.dart'; import 'package:subtracks/database/database.dart';
import 'package:subtracks/services/sync_service.dart'; import 'package:subtracks/services/sync_services.dart';
import 'package:subtracks/sources/subsonic/source.dart'; import 'package:subtracks/sources/subsonic/source.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';

View File

@ -5,7 +5,7 @@ import 'package:test/test.dart';
import '../util/subsonic.dart'; import '../util/subsonic.dart';
void main() { void main() {
groupByTestServer((server, client) { groupByTestServer((client) {
late SubsonicSource source; late SubsonicSource source;
setUp(() async { setUp(() async {
@ -29,7 +29,7 @@ void main() {
expect(kosmo.created.compareTo(DateTime.now()), lessThan(0)); expect(kosmo.created.compareTo(DateTime.now()), lessThan(0));
expect(kosmo.coverArt?.length, greaterThan(0)); expect(kosmo.coverArt?.length, greaterThan(0));
expect(kosmo.year, equals(2006)); expect(kosmo.year, equals(2006));
expect(kosmo.starred?.compareTo(DateTime.now()), lessThan(0)); expect(kosmo.starred, isNull);
expect(kosmo.genre, equals('Electronic')); expect(kosmo.genre, equals('Electronic'));
final retro = items.firstWhere( final retro = items.firstWhere(
@ -39,9 +39,6 @@ void main() {
(a) => a.name == "I Don't Know What I'm Doing", (a) => a.name == "I Don't Know What I'm Doing",
); );
expect(retro.starred, isNull);
expect(dunno.starred, isNull);
expect(kosmo.recentRank, equals(0)); expect(kosmo.recentRank, equals(0));
expect(kosmo.frequentRank, equals(1)); expect(kosmo.frequentRank, equals(1));
@ -56,19 +53,6 @@ void main() {
final items = await source.allArtists().toList(); final items = await source.allArtists().toList();
expect(items.length, equals(2)); expect(items.length, equals(2));
final brad = items.firstWhere((a) => a.name == 'Brad Sucks');
expect(brad.id.length, greaterThan(0));
expect(brad.starred, isNull);
if (![Servers.gonic].contains(server)) {
expect(brad.coverArt?.length, greaterThan(0));
}
final ugress = items.firstWhere((a) => a.name == 'Ugress');
expect(ugress.starred?.compareTo(DateTime.now()), lessThan(0));
}); });
test('allSongs', () async { test('allSongs', () async {

View File

@ -23,14 +23,12 @@ Map<Servers, SubsonicClient> testServerClients() => {
), ),
}; };
void groupByTestServer( void groupByTestServer(void Function(SubsonicClient client) callback) {
void Function(Servers server, SubsonicClient client) callback,
) {
final clients = testServerClients(); final clients = testServerClients();
for (final MapEntry(key: server, value: client) in clients.entries) { for (final MapEntry(key: server, value: client) in clients.entries) {
group(server.name, () { group(server.name, () {
callback(server, client); callback(client);
}); });
} }
} }