diff --git a/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/EventsApi.kt b/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/EventsApi.kt new file mode 100644 index 00000000..dff15f50 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/EventsApi.kt @@ -0,0 +1,31 @@ +package com.fabledsword.minstrel.api.endpoints + +import com.fabledsword.minstrel.models.wire.PlayEndedRequest +import com.fabledsword.minstrel.models.wire.PlayOfflineRequest +import com.fabledsword.minstrel.models.wire.PlaySkippedRequest +import com.fabledsword.minstrel.models.wire.PlayStartedRequest +import com.fabledsword.minstrel.models.wire.PlayStartedResponse +import retrofit2.http.Body +import retrofit2.http.POST + +/** + * Retrofit interface for `POST /api/events`. Mirrors the relevant + * slice of `flutter_client/lib/api/endpoints/events.dart`. All four + * variants share the same URL — the discriminator is in the request + * body's `type` field. Server contract is best-effort per spec; + * callers (the live path in PlayEventsReporter) swallow errors and + * fall back to the offline mutation queue on failure. + */ +interface EventsApi { + @POST("api/events") + suspend fun playStarted(@Body body: PlayStartedRequest): PlayStartedResponse + + @POST("api/events") + suspend fun playEnded(@Body body: PlayEndedRequest) + + @POST("api/events") + suspend fun playSkipped(@Body body: PlaySkippedRequest) + + @POST("api/events") + suspend fun playOffline(@Body body: PlayOfflineRequest) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/wire/EventsWire.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/EventsWire.kt new file mode 100644 index 00000000..f901b11b --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/EventsWire.kt @@ -0,0 +1,51 @@ +package com.fabledsword.minstrel.models.wire + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Wire shapes for `POST /api/events`. The endpoint multiplexes four + * variants on the `type` discriminator field, mirroring + * `flutter_client/lib/api/endpoints/events.dart`. + * + * play_started returns the server-assigned play_event_id (nullable — + * server may suppress under certain conditions); the other three + * variants return 204-equivalent (no body). + */ + +@Serializable +data class PlayStartedRequest( + val type: String = "play_started", + @SerialName("track_id") val trackId: String, + @SerialName("client_id") val clientId: String, + val source: String? = null, +) + +@Serializable +data class PlayStartedResponse( + @SerialName("play_event_id") val playEventId: String? = null, +) + +@Serializable +data class PlayEndedRequest( + val type: String = "play_ended", + @SerialName("play_event_id") val playEventId: String, + @SerialName("duration_played_ms") val durationPlayedMs: Long, +) + +@Serializable +data class PlaySkippedRequest( + val type: String = "play_skipped", + @SerialName("play_event_id") val playEventId: String, + @SerialName("position_ms") val positionMs: Long, +) + +@Serializable +data class PlayOfflineRequest( + val type: String = "play_offline", + @SerialName("track_id") val trackId: String, + @SerialName("client_id") val clientId: String, + val at: String, + @SerialName("duration_played_ms") val durationPlayedMs: Long, + val source: String? = null, +)