Update Conductarr TV contracts and ratings reads
This commit is contained in:
parent
9dd321f07e
commit
f53defad90
13 changed files with 600 additions and 36 deletions
|
|
@ -13,7 +13,7 @@ enum class CachePolicy(internal val lifetimeMillis: Long, internal val capacity:
|
|||
DISCOVERY(30 * 60 * 1000L),
|
||||
RATINGS(6 * 60 * 60 * 1000L, 300),
|
||||
ON_OPEN(Long.MAX_VALUE),
|
||||
SEASON(Long.MAX_VALUE, 300),
|
||||
SEASON(6 * 60 * 60 * 1000L, 300),
|
||||
ACTIVITY(30 * 1000L),
|
||||
NO_CACHE(0),
|
||||
;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ object TvRoutes {
|
|||
private val cast = RouteContract("GET", "${title.path}/cast", "TitleCast")
|
||||
private val related = RouteContract("GET", "${title.path}/related", "Page")
|
||||
private val enrichment = RouteContract("GET", "${title.path}/enrichment", "Enrichment")
|
||||
private val ratings = RouteContract("GET", "${title.path}/ratings", "TitleRatings")
|
||||
private val display = RouteContract("GET", "${title.path}/display", "Display")
|
||||
private val season = RouteContract("GET", "/catalogue/tv/{tmdb}/seasons/{season}", "SeasonDetail")
|
||||
private val performer = RouteContract("GET", "/catalogue/performers/{performer}", "PerformerDetail")
|
||||
private val filmography = RouteContract("GET", "${performer.path}/filmography", "FilmographyPage")
|
||||
|
|
@ -63,7 +65,7 @@ object TvRoutes {
|
|||
|
||||
val contracts: List<RouteContract> = listOf(
|
||||
operator, profiles, feed, dismissals, hide, restore, options, discover, title, cast,
|
||||
related, enrichment, season, performer, filmography, activity, readiness, requests, request, audiences,
|
||||
related, enrichment, ratings, display, season, performer, filmography, activity, readiness, requests, request, audiences,
|
||||
)
|
||||
|
||||
fun operator() = ReadCall(operator, Operator.serializer(), policy = CachePolicy.LAUNCH)
|
||||
|
|
@ -97,7 +99,9 @@ object TvRoutes {
|
|||
|
||||
fun title(tmdb: Long, media: MediaType) = ReadCall(title, Detail.serializer(), titleSegments(tmdb, media), policy = CachePolicy.ON_OPEN)
|
||||
fun cast(tmdb: Long, media: MediaType) = ReadCall(cast, TitleCast.serializer(), titleSegments(tmdb, media), policy = CachePolicy.ON_OPEN)
|
||||
fun enrichment(tmdb: Long, media: MediaType) = ReadCall(enrichment, Enrichment.serializer(), titleSegments(tmdb, media), policy = CachePolicy.RATINGS)
|
||||
fun enrichment(tmdb: Long, media: MediaType) = ReadCall(enrichment, Enrichment.serializer(), titleSegments(tmdb, media), policy = CachePolicy.ON_OPEN)
|
||||
fun ratings(tmdb: Long, media: MediaType) = ReadCall(ratings, TitleRatings.serializer(), titleSegments(tmdb, media), policy = CachePolicy.RATINGS)
|
||||
fun display(tmdb: Long, media: MediaType) = ReadCall(display, Display.serializer(), titleSegments(tmdb, media), policy = CachePolicy.ON_OPEN)
|
||||
fun related(tmdb: Long, media: MediaType, page: Int = 1) = ReadCall(
|
||||
related, Page.serializer(), titleSegments(tmdb, media), mapOf("page" to page.toString()), CachePolicy.ON_OPEN,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.conductarr.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** Display wire contract from the pinned Conductarr schema. */
|
||||
@Serializable
|
||||
data class Display(
|
||||
@SerialName("media_type") val mediaType: MediaType,
|
||||
@SerialName("metadata_state") val metadataState: MetadataState,
|
||||
@SerialName("poster_url") val posterUrl: String? = null,
|
||||
@SerialName("title") val title: String? = null,
|
||||
@SerialName("tmdb_id") val tmdbId: Long,
|
||||
) {
|
||||
@Serializable
|
||||
enum class MediaType { @SerialName("movie") MOVIE, @SerialName("tv") TV }
|
||||
@Serializable
|
||||
enum class MetadataState {
|
||||
@SerialName("observed") OBSERVED,
|
||||
@SerialName("missing") MISSING,
|
||||
@SerialName("unavailable") UNAVAILABLE,
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ data class Episode(
|
|||
@SerialName("number") val number: Long,
|
||||
@SerialName("overview") val overview: String,
|
||||
@SerialName("recorded") val recorded: Recorded,
|
||||
@SerialName("vote_average") val voteAverage: Double? = null,
|
||||
@SerialName("vote_count") val voteCount: Long? = null,
|
||||
) {
|
||||
/** Wire values for recorded. */
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.conductarr.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** TitleRatings wire contract from the pinned Conductarr schema. */
|
||||
@Serializable
|
||||
data class TitleRatings(
|
||||
@SerialName("ratings") val ratings: List<Rating>?,
|
||||
)
|
||||
|
|
@ -13,6 +13,7 @@ class CacheTest {
|
|||
Triple(CachePolicy.DAILY, ReadTrigger.IF_STALE, 86_400_000L),
|
||||
Triple(CachePolicy.DISCOVERY, ReadTrigger.IF_STALE, 1_800_000L),
|
||||
Triple(CachePolicy.RATINGS, ReadTrigger.IF_STALE, 21_600_000L),
|
||||
Triple(CachePolicy.SEASON, ReadTrigger.IF_STALE, 21_600_000L),
|
||||
Triple(CachePolicy.ACTIVITY, ReadTrigger.POLL, 30_000L),
|
||||
)
|
||||
for ((policy, trigger, lifetime) in rows) {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class ClientTest {
|
|||
|
||||
@Test fun `ratings 404 is cached as none for six hours but server failure is not cached`() = runBlocking {
|
||||
val client = client()
|
||||
val call = TvRoutes.enrichment(42, MediaType.MOVIE)
|
||||
val call = TvRoutes.ratings(42, MediaType.MOVIE)
|
||||
server.enqueue(response("missing", 404))
|
||||
assertTrue((client.read(call).toList().single() as ReadResult.Value).saved.absent)
|
||||
now += 6 * 60 * 60 * 1000L - 1
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class ContractTest {
|
|||
for (name in reviewed.keys) assertEquals("Schema constraint drift: $name", reviewed[name], schemas[name])
|
||||
}
|
||||
|
||||
@Test fun `all twenty TV operations exist with their typed request and response contracts`() {
|
||||
@Test fun `all twenty two TV operations exist with their typed request and response contracts`() {
|
||||
val expected = setOf(
|
||||
"GET /api/v1/operator", "GET /api/v1/recommendations/profiles",
|
||||
"GET /api/v1/recommendations/profiles/{profile}/feed",
|
||||
|
|
@ -39,6 +39,7 @@ class ContractTest {
|
|||
"GET /catalogue/options", "GET /catalogue/discover",
|
||||
"GET /catalogue/titles/{tmdb}/{type}", "GET /catalogue/titles/{tmdb}/{type}/cast",
|
||||
"GET /catalogue/titles/{tmdb}/{type}/related", "GET /catalogue/titles/{tmdb}/{type}/enrichment",
|
||||
"GET /catalogue/titles/{tmdb}/{type}/ratings", "GET /catalogue/titles/{tmdb}/{type}/display",
|
||||
"GET /catalogue/tv/{tmdb}/seasons/{season}", "GET /catalogue/performers/{performer}",
|
||||
"GET /catalogue/performers/{performer}/filmography",
|
||||
"GET /api/v1/activity", "GET /api/v1/activity/titles/{tmdb}/{type}",
|
||||
|
|
@ -47,6 +48,19 @@ class ContractTest {
|
|||
)
|
||||
assertEquals(expected, TvRoutes.contracts.map { "${it.method} ${it.path}" }.toSet())
|
||||
assertEquals(expected.size, TvRoutes.contracts.size)
|
||||
val operations = setOf(
|
||||
"get-cockpit-operator", "get-recommendation-profiles", "get-library-audiences",
|
||||
"get-recommendation-feed", "get-catalogue-options", "get-catalogue-discover",
|
||||
"get-recommendation-dismissals", "post-recommendation-dismissal", "delete-recommendation-dismissal",
|
||||
"get-catalogue-title", "get-catalogue-cast", "get-catalogue-related", "get-catalogue-season",
|
||||
"get-catalogue-enrichment", "get-catalogue-display", "get-activity-title", "get-title-requests",
|
||||
"get-catalogue-performer", "get-catalogue-filmography", "post-request-intent", "get-activity",
|
||||
"get-catalogue-ratings",
|
||||
)
|
||||
assertEquals(operations, TvRoutes.contracts.map { route ->
|
||||
document.getValue("paths").jsonObject.getValue(route.path).jsonObject
|
||||
.getValue(route.method.lowercase()).jsonObject.getValue("operationId").jsonPrimitive.content
|
||||
}.toSet())
|
||||
val paths = document["paths"]!!.jsonObject
|
||||
for (route in TvRoutes.contracts) {
|
||||
val operation = paths.getValue(route.path).jsonObject.getValue(route.method.lowercase()).jsonObject
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ internal val modelSerializers: List<KSerializer<*>> = listOf(
|
|||
CastMember.serializer(),
|
||||
Detail.serializer(),
|
||||
Discovery.serializer(),
|
||||
Display.serializer(),
|
||||
Enrichment.serializer(),
|
||||
Episode.serializer(),
|
||||
EpisodeFileChip.serializer(),
|
||||
|
|
@ -72,6 +73,7 @@ internal val modelSerializers: List<KSerializer<*>> = listOf(
|
|||
TitleCast.serializer(),
|
||||
TitleMetadata.serializer(),
|
||||
TitleRequestsBody.serializer(),
|
||||
TitleRatings.serializer(),
|
||||
TitleVersion.serializer(),
|
||||
ViewerReadiness.serializer(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -444,6 +444,16 @@
|
|||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"vote_average": {
|
||||
"description": "Provider episode score on a 0\u201310 scale; absence is not zero.",
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"vote_count": {
|
||||
"description": "Provider episode vote count; explicit zero is retained.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -3169,5 +3179,77 @@
|
|||
"sources"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Display": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"description": "A URL to the JSON Schema for this object.",
|
||||
"examples": [
|
||||
"https://example.com/schemas/Display.json"
|
||||
],
|
||||
"format": "uri",
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"media_type": {
|
||||
"enum": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"metadata_state": {
|
||||
"enum": [
|
||||
"observed",
|
||||
"missing",
|
||||
"unavailable"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"poster_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"tmdb_id": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tmdb_id",
|
||||
"media_type",
|
||||
"metadata_state"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TitleRatings": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"description": "A URL to the JSON Schema for this object.",
|
||||
"examples": [
|
||||
"https://example.com/schemas/TitleRatings.json"
|
||||
],
|
||||
"format": "uri",
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"ratings": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Rating"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ratings"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue