Skip to content

Commit 2de8300

Browse files
committed
fix: avoid long channel pagination urls
1 parent 0aaa605 commit 2de8300

6 files changed

Lines changed: 103 additions & 0 deletions

File tree

openapi.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ paths:
2121
$ref: ./openapi/paths/search.yaml#/Search
2222
/channel:
2323
$ref: ./openapi/paths/channel.yaml#/Channel
24+
/channel/page:
25+
$ref: ./openapi/paths/channel.yaml#/ChannelPage
2426
/podcasts:
2527
$ref: ./openapi/paths/podcasts.yaml#/Podcasts
2628
/podcasts/episodes:
@@ -55,6 +57,8 @@ components:
5557
$ref: ./openapi/components/media.yaml#/SearchPageResponse
5658
ChannelResponse:
5759
$ref: ./openapi/components/media.yaml#/ChannelResponse
60+
ChannelPageRequest:
61+
$ref: ./openapi/components/media.yaml#/ChannelPageRequest
5862
PodcastPageResponse:
5963
$ref: ./openapi/components/media.yaml#/PodcastPageResponse
6064
PodcastEpisodesResponse:

openapi/components/media.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ ChannelResponse:
3838
isVerified: { type: boolean }
3939
videos: { type: array, items: { $ref: '#/VideoItem' } }
4040
nextpage: { type: string, nullable: true }
41+
ChannelPageRequest:
42+
type: object
43+
required: [url]
44+
properties:
45+
url: { type: string }
46+
nextpage: { type: string, nullable: true }
47+
sort:
48+
type: string
49+
nullable: true
50+
enum: [latest, popular, oldest]
4151
PodcastItem:
4252
type: object
4353
required: [id, title, url, thumbnailUrl, uploaderName, streamCount, playlistType]

openapi/paths/channel.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,27 @@ Channel:
3131
$ref: ../components/common.yaml#/JsonError
3232
'422':
3333
$ref: ../components/common.yaml#/JsonError
34+
ChannelPage:
35+
post:
36+
tags: [extraction]
37+
summary: Get channel metadata and videos with pagination cursor in JSON body
38+
requestBody:
39+
required: true
40+
content:
41+
application/json:
42+
schema:
43+
$ref: ../components/media.yaml#/ChannelPageRequest
44+
responses:
45+
'200':
46+
description: Channel metadata and video page.
47+
headers:
48+
X-Request-ID:
49+
$ref: ../components/common.yaml#/RequestIdHeader
50+
content:
51+
application/json:
52+
schema:
53+
$ref: ../components/media.yaml#/ChannelResponse
54+
'400':
55+
$ref: ../components/common.yaml#/JsonError
56+
'422':
57+
$ref: ../components/common.yaml#/JsonError
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.typetype.server.models
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class ChannelPageRequest(
7+
val url: String? = null,
8+
val nextpage: String? = null,
9+
val sort: String? = null,
10+
)

src/main/kotlin/dev/typetype/server/routes/ChannelRoutes.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package dev.typetype.server.routes
22

3+
import dev.typetype.server.models.ChannelPageRequest
4+
import dev.typetype.server.models.ChannelResponse
35
import dev.typetype.server.models.ErrorResponse
46
import dev.typetype.server.models.ExtractionResult
57
import dev.typetype.server.services.ChannelService
68
import io.ktor.http.HttpStatusCode
9+
import io.ktor.server.application.ApplicationCall
10+
import io.ktor.server.request.receive
711
import io.ktor.server.response.respond
812
import io.ktor.server.routing.Route
913
import io.ktor.server.routing.get
14+
import io.ktor.server.routing.post
1015

1116
fun Route.channelRoutes(channelService: ChannelService) {
1217
get("/channel") {
@@ -21,4 +26,25 @@ fun Route.channelRoutes(channelService: ChannelService) {
2126
is ExtractionResult.Failure -> call.respond(HttpStatusCode.UnprocessableEntity, ErrorResponse(result.message))
2227
}
2328
}
29+
post("/channel/page") {
30+
val request = call.receive<ChannelPageRequest>()
31+
val url = request.url?.takeIf { it.isNotBlank() }
32+
?: return@post call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing 'url' parameter"))
33+
34+
call.respondChannelResult(
35+
channelService.getChannel(
36+
url = url,
37+
nextpage = request.nextpage?.takeIf { it.isNotBlank() },
38+
sort = request.sort?.takeIf { it.isNotBlank() },
39+
)
40+
)
41+
}
42+
}
43+
44+
private suspend fun ApplicationCall.respondChannelResult(result: ExtractionResult<ChannelResponse>) {
45+
when (result) {
46+
is ExtractionResult.Success -> respond(result.data)
47+
is ExtractionResult.BadRequest -> respond(HttpStatusCode.BadRequest, ErrorResponse(result.message))
48+
is ExtractionResult.Failure -> respond(HttpStatusCode.UnprocessableEntity, ErrorResponse(result.message))
49+
}
2450
}

src/test/kotlin/dev/typetype/server/ChannelRoutesTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import dev.typetype.server.models.ExtractionResult
55
import dev.typetype.server.routes.channelRoutes
66
import dev.typetype.server.services.ChannelService
77
import io.ktor.client.request.get
8+
import io.ktor.client.request.header
9+
import io.ktor.client.request.post
10+
import io.ktor.client.request.setBody
811
import io.ktor.http.HttpStatusCode
12+
import io.ktor.http.HttpHeaders
913
import io.ktor.serialization.kotlinx.json.json
1014
import io.ktor.server.application.install
1115
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
@@ -80,4 +84,29 @@ class ChannelRoutesTest {
8084
assertEquals(HttpStatusCode.OK, response.status)
8185
coVerify { channelService.getChannel("https://www.youtube.com/@test/search?query=typing", null, null) }
8286
}
87+
88+
@Test
89+
fun `POST channel page accepts long nextpage in body`() = withApp {
90+
val nextpage = "x".repeat(9_000)
91+
coEvery { channelService.getChannel(any(), any(), any()) } returns
92+
ExtractionResult.Success(testChannelResponse())
93+
94+
val response = client.post("/channel/page") {
95+
header(HttpHeaders.ContentType, "application/json")
96+
setBody("""{"url":"https://youtube.com/channel/test","nextpage":"$nextpage","sort":"latest"}""")
97+
}
98+
99+
assertEquals(HttpStatusCode.OK, response.status)
100+
coVerify { channelService.getChannel("https://youtube.com/channel/test", nextpage, "latest") }
101+
}
102+
103+
@Test
104+
fun `POST channel page without url returns 400`() = withApp {
105+
val response = client.post("/channel/page") {
106+
header(HttpHeaders.ContentType, "application/json")
107+
setBody("""{"nextpage":"cursor"}""")
108+
}
109+
110+
assertEquals(HttpStatusCode.BadRequest, response.status)
111+
}
83112
}

0 commit comments

Comments
 (0)