Files
bvandeusen d67c0de596 refactor(playlists): #411 R2 — generic registry-driven system endpoints
Replaces the per-kind refresh/shuffle handlers with one generic
pair driven off the kind registry, in lockstep across both clients.

Server:
- systemPlaylistKind gains Singleton; RefreshableSystemKind(key)
  exported. for_you/discover singleton; songs_like_artist not.
- New generic POST /api/playlists/system/{kind}/refresh and
  GET /api/playlists/system/{kind}/shuffle ({kind} = raw
  system_variant). Non-singleton/unknown kind → 404. Deleted
  playlists_{foryou,discover}_refresh.go and the per-kind shuffle
  wrappers; serveSystemPlaylistShuffle core kept.
- playlistRowView.refreshable: server-derived flag so clients show
  the refresh affordance generically without hardcoding kinds.

Web (not drift-cached → uses the server flag):
- refreshSystem(variant) replaces refreshForYou/refreshDiscover;
  systemShuffle drops the for_you→for-you mapping (raw variant).
- PlaylistCard + detail page gate the kebab/Refresh button on
  playlist.refreshable; label is "Refresh {name}". Tests reworked;
  obsolete refresh-foryou/discover api tests deleted.

Flutter (list tiles are drift-cache-sourced → derive, no migration):
- Playlist.refreshable getter = isSystem && variant !=
  songs_like_artist (holds for all current + planned kinds).
- refreshSystem/systemShuffle use the raw variant; PlaylistCard +
  detail screen gate kebab/Regenerate/source-tagging on refreshable
  so songs_like_artist plays via get() (no by-kind endpoint).

Pure-plumbing refactor; CI verifies parity. Next (R3): the five
discovery mixes — each a candidate query + one registry entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:21:09 -04:00

81 lines
3.0 KiB
Dart

import 'package:dio/dio.dart';
import '../../models/playlist.dart';
/// Wire shape for GET /api/playlists. Server splits owned vs. public so
/// the UI can present them as different sections; we preserve that split
/// to give the integrations page room to grow.
class PlaylistsList {
const PlaylistsList({required this.owned, required this.public});
final List<Playlist> owned;
final List<Playlist> public;
factory PlaylistsList.empty() =>
const PlaylistsList(owned: [], public: []);
/// Concatenated view for callers that don't care about the split.
List<Playlist> get all => [...owned, ...public];
}
class PlaylistsApi {
PlaylistsApi(this._dio);
final Dio _dio;
/// GET /api/playlists?kind=user|system|all. Server returns
/// `{"owned": [...], "public": [...]}`. Owned is the caller's own
/// playlists, filtered by the kind param (default "user"). Public
/// is other users' shared playlists; not filtered by kind.
Future<PlaylistsList> list({String kind = 'user'}) async {
final r = await _dio.get<Map<String, dynamic>>(
'/api/playlists',
queryParameters: {'kind': kind},
);
final body = r.data ?? const <String, dynamic>{};
List<Playlist> parse(String key) {
final raw = (body[key] as List?) ?? const [];
return raw
.map((e) => Playlist.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false);
}
return PlaylistsList(owned: parse('owned'), public: parse('public'));
}
Future<PlaylistDetail> get(String id) async {
final r = await _dio.get<Map<String, dynamic>>('/api/playlists/$id');
return PlaylistDetail.fromJson(r.data ?? const {});
}
/// GET /api/playlists/system/{kind}/shuffle (#415 / #411 R2).
/// Same shape as get() but tracks are server-ordered rotation-aware
/// (unplayed-this-rotation first; resets when exhausted). {kind} is
/// the raw system_variant. Intentionally uncached — varies per play.
Future<PlaylistDetail> systemShuffle(String variant) async {
final r = await _dio.get<Map<String, dynamic>>(
'/api/playlists/system/$variant/shuffle',
);
return PlaylistDetail.fromJson(r.data ?? const {});
}
/// POST /api/playlists/{id}/tracks. Owner only; server returns the
/// playlist detail with the new rows.
Future<void> appendTracks(String playlistId, List<String> trackIds) async {
await _dio.post<void>(
'/api/playlists/$playlistId/tracks',
data: {'track_ids': trackIds},
);
}
/// POST /api/playlists/system/{kind}/refresh (#411 R2). Rebuilds
/// the caller's system playlists and returns the named kind's new
/// playlist id, or null when the library is empty. {kind} is the
/// raw system_variant — the server routes generically off the
/// kind registry, no hyphen mapping.
Future<String?> refreshSystem(String variant) async {
final r = await _dio.post<Map<String, dynamic>>(
'/api/playlists/system/$variant/refresh',
data: const <String, dynamic>{},
);
return (r.data ?? const {})['playlist_id'] as String?;
}
}