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
|
package com.fabledsword.minstrel.api
|
||||||
|
|
||||||
import com.fabledsword.minstrel.BuildConfig
|
import com.fabledsword.minstrel.BuildConfig
|
||||||
|
import com.fabledsword.minstrel.api.endpoints.LibraryApi
|
||||||
import com.fabledsword.minstrel.auth.AuthStore
|
import com.fabledsword.minstrel.auth.AuthStore
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -70,6 +71,11 @@ object NetworkModule {
|
|||||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideLibraryApi(retrofit: Retrofit): LibraryApi =
|
||||||
|
retrofit.create(LibraryApi::class.java)
|
||||||
|
|
||||||
private const val CONNECT_TIMEOUT_SECONDS = 10L
|
private const val CONNECT_TIMEOUT_SECONDS = 10L
|
||||||
private const val READ_TIMEOUT_SECONDS = 30L
|
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