fix(flutter): restore artist coverUrl on drift-cached ArtistRef

Multi-artist surfaces (home Rediscover, Library Artists tab, Liked
Artists carousel) were all rendering the music-notes placeholder
instead of real artist covers. Root cause:

CachedArtist.toRef() returned an ArtistRef with empty coverUrl —
the cache doesn't store the representative album id the server
derives at query time, and the adapter never reconstructed it.
The artist detail screen worked coincidentally because it derives
its header cover from `artist.albums[0].id` directly rather than
the ArtistRef's coverUrl field.

Fix:
* CachedArtistAdapter.toRef() now accepts an optional coverAlbumId
  and reconstructs `/api/albums/<id>/cover` when given. Matches the
  pattern AlbumRef uses (deterministic URL from entity id).
* artistTileProvider, libraryArtistsProvider, and artistProvider
  (single-artist) each LEFT JOIN cached_albums ordered by sort_title.
  First row per artist carries the alphabetically-first album id;
  toRef projects that into the cover URL.
* Multi-artist queries dedup in toResult since the join multiplies
  rows by album count.

Artists with no albums yet in drift come through with empty
coverUrl — UI falls back to the music-notes placeholder, same
behavior as before for that legitimately-coverless state.
This commit is contained in:
2026-05-14 12:01:02 -04:00
parent 2a18e91c39
commit 8cb9a8b797
4 changed files with 78 additions and 13 deletions
+12 -1
View File
@@ -17,10 +17,21 @@ import '../models/track.dart';
import 'db.dart';
extension CachedArtistAdapter on CachedArtist {
ArtistRef toRef() => ArtistRef(
/// `coverAlbumId` lets the caller pass a representative album id so
/// the ArtistRef carries a reconstructed `/api/albums/<id>/cover`
/// URL. cached_artists doesn't store this directly — the server
/// derives it from the most-recent album at query time — so drift
/// readers that want the cover URL join cached_albums and pass the
/// first album's id through. Empty string yields empty coverUrl,
/// matching the server's behavior for endpoints that don't carry
/// the lookup (artist detail, search, raw cached_artists row reads).
ArtistRef toRef({String coverAlbumId = ''}) => ArtistRef(
id: id,
name: name,
sortName: sortName,
coverUrl: coverAlbumId.isNotEmpty
? '/api/albums/$coverAlbumId/cover'
: '',
);
}
+19 -2
View File
@@ -18,6 +18,7 @@
// subscriptions; drift handles this fine in practice but worth
// measuring if a future surface scales to hundreds.
import 'package:drift/drift.dart' as drift show OrderingTerm;
import 'package:drift/drift.dart' show leftOuterJoin;
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -63,6 +64,12 @@ final albumTileProvider =
/// Watches the cached_artists row for [id]. Triggers background
/// hydration on miss; yields the row once it lands.
///
/// LEFT JOIN cached_albums ordered by sort_title so the first row per
/// artist carries a representative album id; toRef() reconstructs
/// `/api/albums/<id>/cover` from it. Without this join the ArtistRef
/// has an empty coverUrl and tiles fall back to the music-notes
/// placeholder even though the server has the cover available.
final artistTileProvider =
StreamProvider.family<ArtistRef?, String>((ref, id) async* {
if (id.isEmpty) {
@@ -70,7 +77,12 @@ final artistTileProvider =
return;
}
final db = ref.watch(appDbProvider);
final query = db.select(db.cachedArtists)..where((t) => t.id.equals(id));
final query = (db.select(db.cachedArtists)..where((t) => t.id.equals(id)))
.join([
leftOuterJoin(db.cachedAlbums,
db.cachedAlbums.artistId.equalsExp(db.cachedArtists.id)),
])
..orderBy([drift.OrderingTerm.asc(db.cachedAlbums.sortTitle)]);
var enqueued = false;
await for (final rows in query.watch()) {
if (rows.isEmpty) {
@@ -83,7 +95,12 @@ final artistTileProvider =
yield null;
continue;
}
yield rows.first.toRef();
// First row has the alphabetically-first album (or null if artist
// has no albums in drift yet — yields a coverless ArtistRef which
// the UI falls back to the placeholder icon for).
final artist = rows.first.readTable(db.cachedArtists);
final firstAlbum = rows.first.readTableOrNull(db.cachedAlbums);
yield artist.toRef(coverAlbumId: firstAlbum?.id ?? '');
}
});