feat(android): LibraryApi Retrofit interface + wire types (M8 phase 5.1)
Mirrors flutter_client/lib/api/endpoints/library.dart 1:1.
Wire types (snake_case @SerialName per server JSON):
- TrackWire — id/title/album/artist/duration/streamUrl + nullable
track/disc numbers (fields verified against TrackRef.fromJson in
flutter_client/lib/models/track.dart)
- ArtistWire / ArtistDetailWire — the detail shape embeds "albums"
- AlbumWire / AlbumDetailWire — the detail shape embeds "tracks"
The Detail variants are explicit data classes (rather than a generic
envelope) because the server returns ArtistRef fields PLUS the
embedded array in the same object, which kotlinx.serialization can't
deserialize through a polymorphic envelope.
LibraryApi endpoints:
- getTrack(id)
- getArtistDetail(id) — ArtistDetailWire
- getArtistTracks(id) — bare List<TrackWire> (server emits a bare
array, NOT enveloped; Retrofit handles it via List return type)
- getAlbumDetail(id) — AlbumDetailWire
- shuffleLibrary(limit) — bare List<TrackWire>
Home endpoints (/api/home and /api/home/index) deferred to a future
HomeApi file because they have their own (larger) wire types that
only the Home screen consumes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.fabledsword.minstrel.api
|
||||
|
||||
import com.fabledsword.minstrel.BuildConfig
|
||||
import com.fabledsword.minstrel.api.endpoints.LibraryApi
|
||||
import com.fabledsword.minstrel.auth.AuthStore
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Module
|
||||
@@ -70,6 +71,11 @@ object NetworkModule {
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLibraryApi(retrofit: Retrofit): LibraryApi =
|
||||
retrofit.create(LibraryApi::class.java)
|
||||
|
||||
private const val CONNECT_TIMEOUT_SECONDS = 10L
|
||||
private const val READ_TIMEOUT_SECONDS = 30L
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.fabledsword.minstrel.api.endpoints
|
||||
|
||||
import com.fabledsword.minstrel.models.wire.AlbumDetailWire
|
||||
import com.fabledsword.minstrel.models.wire.ArtistDetailWire
|
||||
import com.fabledsword.minstrel.models.wire.TrackWire
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* Retrofit interface for the server's native /api/* library surface.
|
||||
* Mirrors `flutter_client/lib/api/endpoints/library.dart` 1:1.
|
||||
*
|
||||
* Notes on shapes:
|
||||
* - GET /api/artists/{id} returns ArtistDetailWire (ArtistRef fields
|
||||
* + embedded "albums" array)
|
||||
* - GET /api/artists/{id}/tracks returns a bare JSON array, not an
|
||||
* enveloped object — Retrofit's `List<TrackWire>` return type
|
||||
* handles that.
|
||||
* - GET /api/albums/{id} returns AlbumDetailWire (AlbumRef fields
|
||||
* + embedded "tracks" array)
|
||||
* - GET /api/library/shuffle returns a bare JSON array.
|
||||
*
|
||||
* Home endpoints (/api/home, /api/home/index) live in a separate
|
||||
* `HomeApi` because they have their own wire types (HomePayload,
|
||||
* HomeIndex) that are larger and only used by the Home screen.
|
||||
*/
|
||||
interface LibraryApi {
|
||||
@GET("api/tracks/{id}")
|
||||
suspend fun getTrack(@Path("id") id: String): TrackWire
|
||||
|
||||
@GET("api/artists/{id}")
|
||||
suspend fun getArtistDetail(@Path("id") id: String): ArtistDetailWire
|
||||
|
||||
@GET("api/artists/{id}/tracks")
|
||||
suspend fun getArtistTracks(@Path("id") id: String): List<TrackWire>
|
||||
|
||||
@GET("api/albums/{id}")
|
||||
suspend fun getAlbumDetail(@Path("id") id: String): AlbumDetailWire
|
||||
|
||||
@GET("api/library/shuffle")
|
||||
suspend fun shuffleLibrary(@Query("limit") limit: Int = 100): List<TrackWire>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.fabledsword.minstrel.models.wire
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Wire shape for `AlbumRef`. Mirrors `flutter_client/lib/models/album.dart`.
|
||||
*/
|
||||
@Serializable
|
||||
data class AlbumWire(
|
||||
val id: String,
|
||||
val title: String,
|
||||
@SerialName("artist_id") val artistId: String,
|
||||
@SerialName("sort_title") val sortTitle: String = "",
|
||||
@SerialName("artist_name") val artistName: String = "",
|
||||
val year: Int? = null,
|
||||
@SerialName("track_count") val trackCount: Int = 0,
|
||||
@SerialName("duration_sec") val durationSec: Int = 0,
|
||||
@SerialName("cover_url") val coverUrl: String = "",
|
||||
)
|
||||
|
||||
/**
|
||||
* Wire shape for `GET /api/albums/{id}` — server returns AlbumRef
|
||||
* fields PLUS an embedded `tracks` array.
|
||||
*/
|
||||
@Serializable
|
||||
data class AlbumDetailWire(
|
||||
val id: String,
|
||||
val title: String,
|
||||
@SerialName("artist_id") val artistId: String,
|
||||
@SerialName("sort_title") val sortTitle: String = "",
|
||||
@SerialName("artist_name") val artistName: String = "",
|
||||
val year: Int? = null,
|
||||
@SerialName("track_count") val trackCount: Int = 0,
|
||||
@SerialName("duration_sec") val durationSec: Int = 0,
|
||||
@SerialName("cover_url") val coverUrl: String = "",
|
||||
val tracks: List<TrackWire> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.fabledsword.minstrel.models.wire
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Wire shape for `ArtistRef`. Mirrors `flutter_client/lib/models/artist.dart`.
|
||||
*/
|
||||
@Serializable
|
||||
data class ArtistWire(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@SerialName("sort_name") val sortName: String = "",
|
||||
@SerialName("album_count") val albumCount: Int = 0,
|
||||
@SerialName("cover_url") val coverUrl: String = "",
|
||||
)
|
||||
|
||||
/**
|
||||
* Wire shape for `GET /api/artists/{id}` — server returns ArtistRef
|
||||
* fields PLUS an embedded `albums` array. We can't directly map this
|
||||
* to ArtistWire because Retrofit/kotlinx.serialization doesn't support
|
||||
* a single record being two types; an explicit DetailWire spells out
|
||||
* both halves and the repository handles the split.
|
||||
*/
|
||||
@Serializable
|
||||
data class ArtistDetailWire(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@SerialName("sort_name") val sortName: String = "",
|
||||
@SerialName("album_count") val albumCount: Int = 0,
|
||||
@SerialName("cover_url") val coverUrl: String = "",
|
||||
val albums: List<AlbumWire> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.fabledsword.minstrel.models.wire
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Wire shape for `TrackRef` as the server emits it. Mirrors
|
||||
* `flutter_client/lib/models/track.dart`'s `TrackRef.fromJson`
|
||||
* field-for-field; the keys are snake_case because the server is Go
|
||||
* (json:"album_id" etc.).
|
||||
*
|
||||
* Mappers in feature packages translate this to the domain TrackRef
|
||||
* data class and the `cached_tracks` Room entity. Default values match
|
||||
* the Flutter Dart-side "?? 0" / "?? ''" fallbacks for forward-compat
|
||||
* with sparse server responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class TrackWire(
|
||||
val id: String,
|
||||
val title: String,
|
||||
@SerialName("album_id") val albumId: String,
|
||||
@SerialName("album_title") val albumTitle: String = "",
|
||||
@SerialName("artist_id") val artistId: String,
|
||||
@SerialName("artist_name") val artistName: String = "",
|
||||
@SerialName("track_number") val trackNumber: Int? = null,
|
||||
@SerialName("disc_number") val discNumber: Int? = null,
|
||||
@SerialName("duration_sec") val durationSec: Int = 0,
|
||||
@SerialName("stream_url") val streamUrl: String = "",
|
||||
)
|
||||
Reference in New Issue
Block a user