Files
minstrel/flutter_client/lib/api/endpoints/library.dart
T
bvandeusen 3e267918b7 feat(flutter/library): API endpoints + Riverpod providers
LibraryApi wraps GET /api/home, /api/artists/{id}(/tracks),
/api/albums/{id}. dioProvider builds an authenticated dio (token
resolver reads session_token from secure storage on every request).
homeProvider, artistProvider(id), albumProvider(id) sit on top.
2026-05-02 17:26:57 -04:00

72 lines
3.1 KiB
Dart

import 'package:dio/dio.dart';
import '../../models/album.dart';
import '../../models/artist.dart';
import '../../models/home_data.dart';
import '../../models/track.dart';
/// LibraryApi wraps the server's native /api/* library surface.
///
/// Response shapes (verified against internal/api/library.go,
/// internal/api/library_albums.go, internal/api/home.go):
///
/// GET /api/home → HomePayload (flat sections)
/// GET /api/artists/{id} → ArtistDetail (ArtistRef fields + "albums")
/// GET /api/artists/{id}/tracks → flat []TrackRef array (NOT enveloped)
/// GET /api/albums/{id} → AlbumDetail (AlbumRef fields + "tracks")
///
/// The artist-tracks endpoint deviates from the plan-text starter: the
/// server emits a bare JSON array, not `{"tracks": [...]}`. We parse the
/// top-level response as `List` accordingly.
class LibraryApi {
LibraryApi(this._dio);
final Dio _dio;
Future<HomeData> getHome() async {
final r = await _dio.get<Map<String, dynamic>>('/api/home');
return HomeData.fromJson(r.data ?? const {});
}
/// GET /api/artists/{id}. Server returns ArtistDetail which embeds
/// ArtistRef inline; ArtistRef.fromJson already reads only the fields
/// it cares about, so passing the whole body is correct.
Future<ArtistRef> getArtist(String id) async {
final r = await _dio.get<Map<String, dynamic>>('/api/artists/$id');
return ArtistRef.fromJson(r.data ?? const {});
}
/// Pulls the "albums" array out of the same ArtistDetail body. Callers
/// that need both the artist and its albums should issue two provider
/// reads (artistProvider + artistAlbumsProvider) — both hit the same
/// underlying URL and dio's response is not memoized here, but the
/// Riverpod layer caches per-id so cost stays at one round-trip.
Future<List<AlbumRef>> getArtistAlbums(String id) async {
final r = await _dio.get<Map<String, dynamic>>('/api/artists/$id');
final raw = (r.data?['albums'] as List?) ?? const [];
return raw
.map((e) => AlbumRef.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false);
}
/// GET /api/artists/{id}/tracks. Server emits a bare JSON array, so we
/// type the response as `List<dynamic>` rather than a Map envelope.
Future<List<TrackRef>> getArtistTracks(String id) async {
final r = await _dio.get<List<dynamic>>('/api/artists/$id/tracks');
final raw = r.data ?? const [];
return raw
.map((e) => TrackRef.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false);
}
/// GET /api/albums/{id}. Returns the album ref alongside its track list
/// in a single record so screens can render both without a second fetch.
Future<({AlbumRef album, List<TrackRef> tracks})> getAlbum(String id) async {
final r = await _dio.get<Map<String, dynamic>>('/api/albums/$id');
final body = r.data ?? const <String, dynamic>{};
final tracks = ((body['tracks'] as List?) ?? const [])
.map((e) => TrackRef.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false);
return (album: AlbumRef.fromJson(body), tracks: tracks);
}
}