feat(android): EventsApi + wire types for /api/events

Retrofit interface + the four request body variants
(play_started / play_ended / play_skipped / play_offline)
matching flutter_client/lib/api/endpoints/events.dart.
play_started's response carries play_event_id (nullable).

Not wired into any caller yet — PlayEventsReporter lands later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 14:01:44 -04:00
parent 415200d8f0
commit 92c128e388
2 changed files with 82 additions and 0 deletions
@@ -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)
}
@@ -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,
)