fix(flutter): reconcile stale playlists + recover from 404 on tap

The 404 on tapping the For-You tile traced to stale drift rows.
BuildSystemPlaylists rotates UUIDs on every rebuild, so old For-You
/ Songs-Like ids accumulate in cachedPlaylists. The list provider's
fetchAndPopulate was only doing insertOrReplace, which adds new rows
but never removes the obsolete ones — so the home tile renders 8
playlists when the server only knows 4, and 4 of those tiles 404 on
tap.

Two fixes:

playlistsListProvider.fetchAndPopulate now reconciles. After
fetching the fresh list, deleteWhere any user-owned drift row whose
id isn't in the fresh response, then upsert the fresh set in the
same batch. Public-from-others rows are left alone — they're not
keyed by ownership and we don't want to drop someone else's public
playlist just because the current user's response didn't enumerate
it. Operates inside the existing batch so it's atomic.

playlistDetailProvider.fetchAndPopulate now treats a DioException
404 as "this row is stale": delete the cachedPlaylists + any
cachedPlaylistTracks rows for this id, return false so the UI yields
emptyDetail. The next render of the home row sees the row gone and
the tile disappears, completing the cleanup.

Side note: every system-playlist rebuild discards drift rows for the
just-evicted UUIDs and writes the new ones. That cycle's been
silently churning since system playlists shipped — this is the
first time the cleanup actually runs.
This commit is contained in:
2026-05-11 18:32:40 -04:00
parent af5744f8ab
commit 8d466ebdd5
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart' as drift;
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -44,7 +45,21 @@ final playlistsListProvider =
debugPrint(
'playlistsListProvider($kind): wire returned owned=${fresh.owned.length} '
'(system=$ownedSysCount) public=${fresh.public.length}');
// Reconcile: BuildSystemPlaylists rotates system-playlist UUIDs
// every rebuild, so insertOrReplace alone leaves stale rows in
// drift. Tapping one of those stale tiles 404s. Delete every
// owned drift row whose id isn't in the fresh response, then
// upsert the fresh set.
final freshOwnedIds =
fresh.owned.map((p) => p.id).toSet();
await db.batch((b) {
if (user != null) {
b.deleteWhere(db.cachedPlaylists, (t) {
return t.userId.equals(user.id) &
t.id.isNotIn(freshOwnedIds);
});
}
for (final p in fresh.all) {
b.insert(db.cachedPlaylists, p.toDrift(),
mode: drift.InsertMode.insertOrReplace);
@@ -216,6 +231,21 @@ final playlistDetailProvider =
'playlistDetailProvider($id): drift write done; awaiting watch re-emit');
return true;
} catch (e, st) {
// 404 = the playlist row in drift is stale (BuildSystemPlaylists
// rotates UUIDs on each rebuild, so old For-You / Songs-Like
// tiles can outlive the actual server-side playlist). Wipe the
// stale row + its track positions so the home tile disappears
// on next render and we don't keep trying.
if (e is DioException && e.response?.statusCode == 404) {
debugPrint(
'playlistDetailProvider($id): server says 404, evicting stale drift rows');
await db.batch((b) {
b.deleteWhere(
db.cachedPlaylistTracks, (t) => t.playlistId.equals(id));
b.deleteWhere(db.cachedPlaylists, (t) => t.id.equals(id));
});
return false;
}
debugPrint('playlistDetailProvider($id): fetch failed: $e\n$st');
return false;
}