Add Kotlin Multiplatform mobile client
A Compose Multiplatform app (mobile/) targeting Android, iOS, and desktop,
sharing one Kotlin codebase: OIDC login via PKCE against the same Zitadel
instance the web player uses (loopback redirect on desktop, a musify://
custom scheme on mobile), a token store per platform, and a first pass at
browsing/streaming tracks through the existing API.
deploy/compose.yml's zitadel-init gains a third OIDC app registration
("Musify Native", public client, no secret) alongside the existing API and
web apps, writing its client id to NATIVE_CLIENT_ID in the env file the same
way the other two already do.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package com.musify.app
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
/** Mirrors `Musify.Application.Tracks.Responses.TrackApplicationResponse`. */
|
||||
@Serializable
|
||||
data class TrackApplicationResponse(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val artist: String? = null,
|
||||
val source: String,
|
||||
val externalId: String? = null,
|
||||
val audioStatus: String,
|
||||
val duration: Double,
|
||||
val listensCount: Int,
|
||||
val ownerUserId: Long? = null,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Mirrors `TrackSearchItemResponse`: exactly one of [track] / [youTubeSong] is
|
||||
* populated. The demo only lists local catalog tracks, so [youTubeSong] is
|
||||
* kept untyped (raw JSON) rather than modeled in full.
|
||||
*/
|
||||
@Serializable
|
||||
data class TrackSearchItemResponse(
|
||||
val source: String,
|
||||
val track: TrackApplicationResponse? = null,
|
||||
val youTubeSong: JsonElement? = null
|
||||
)
|
||||
|
||||
/** Mirrors `TracksSearchResponse`. */
|
||||
@Serializable
|
||||
data class TracksSearchResponse(
|
||||
val items: List<TrackSearchItemResponse>,
|
||||
val pageNumber: Int,
|
||||
val pageSize: Int,
|
||||
val pageCount: Int,
|
||||
val totalItemCount: Int,
|
||||
val hasPreviousPage: Boolean,
|
||||
val hasNextLocalPage: Boolean,
|
||||
val nextYoutubeContinuationToken: String? = null,
|
||||
val youtubeUnavailable: Boolean = false,
|
||||
val hasNextPage: Boolean = false
|
||||
)
|
||||
|
||||
/** Mirrors `TrackStreamResponse`: a manifest URL plus a short-lived signed ticket. */
|
||||
@Serializable
|
||||
data class TrackStreamResponse(
|
||||
val manifestUrl: String,
|
||||
val ticket: String,
|
||||
val expiresInSeconds: Int
|
||||
)
|
||||
|
||||
/** The final URL the streaming gateway accepts: `ManifestUrl?t=Ticket` (see TicketValidationMiddleware). */
|
||||
fun TrackStreamResponse.playableUrl(): String {
|
||||
val separator = if (manifestUrl.contains("?")) "&" else "?"
|
||||
return "$manifestUrl${separator}t=$ticket"
|
||||
}
|
||||
Reference in New Issue
Block a user