import 'package:dio/dio.dart'; import '../../models/album.dart'; import '../../models/artist.dart'; import '../../models/home_data.dart'; import '../../models/home_index.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 getHome() async { final r = await _dio.get>('/api/home'); return HomeData.fromJson(r.data ?? const {}); } /// GET /api/home/index — per-item rendering variant. Returns just IDs /// per section; client hydrates each tile via the per-entity /// endpoints. ~10× smaller than /api/home on populated libraries so /// the cold-visit round-trip is correspondingly short. Future getHomeIndex() async { final r = await _dio.get>('/api/home/index'); return HomeIndex.fromJson(r.data ?? const {}); } /// GET /api/tracks/{id}. Returns the canonical TrackRef. Used by /// the HydrationQueue to populate cached_tracks on a per-tile miss /// — the existing endpoint already joins album + artist so the /// response carries everything TrackRef needs. Future getTrack(String id) async { final r = await _dio.get>('/api/tracks/$id'); return TrackRef.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 getArtist(String id) async { final r = await _dio.get>('/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> getArtistAlbums(String id) async { final r = await _dio.get>('/api/artists/$id'); final raw = (r.data?['albums'] as List?) ?? const []; return raw .map((e) => AlbumRef.fromJson((e as Map).cast())) .toList(growable: false); } /// GET /api/artists/{id}/tracks. Server emits a bare JSON array, so we /// type the response as `List` rather than a Map envelope. Future> getArtistTracks(String id) async { final r = await _dio.get>('/api/artists/$id/tracks'); final raw = r.data ?? const []; return raw .map((e) => TrackRef.fromJson((e as Map).cast())) .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 tracks})> getAlbum(String id) async { final r = await _dio.get>('/api/albums/$id'); final body = r.data ?? const {}; final tracks = ((body['tracks'] as List?) ?? const []) .map((e) => TrackRef.fromJson((e as Map).cast())) .toList(growable: false); return (album: AlbumRef.fromJson(body), tracks: tracks); } }