Files
Musify/mobile/composeApp/src/commonMain/kotlin/com/musify/app/Models.kt
T
Admin c5b2b71eea 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.
2026-09-10 10:03:11 +02:00

62 lines
1.8 KiB
Kotlin

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"
}