diff --git a/android/app/src/main/java/com/fabledsword/minstrel/api/NetworkModule.kt b/android/app/src/main/java/com/fabledsword/minstrel/api/NetworkModule.kt index bde4b927..96ce4cdb 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/api/NetworkModule.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/api/NetworkModule.kt @@ -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 } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/LibraryApi.kt b/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/LibraryApi.kt new file mode 100644 index 00000000..6d53c676 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/LibraryApi.kt @@ -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` 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 + + @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 +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/wire/AlbumWire.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/AlbumWire.kt new file mode 100644 index 00000000..fbafb846 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/AlbumWire.kt @@ -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 = emptyList(), +) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/wire/ArtistWire.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/ArtistWire.kt new file mode 100644 index 00000000..dbf4283b --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/ArtistWire.kt @@ -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 = emptyList(), +) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/wire/TrackWire.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/TrackWire.kt new file mode 100644 index 00000000..dba0ab70 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/TrackWire.kt @@ -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 = "", +)