Skip to content

Commit 23a2229

Browse files
committed
test: cover audio-only source probing
1 parent 0bc9e12 commit 23a2229

5 files changed

Lines changed: 107 additions & 5 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package dev.typetype.server
2+
3+
import dev.typetype.server.models.ExtractionResult
4+
import dev.typetype.server.models.ProxyResponse
5+
import dev.typetype.server.routes.audioOnlyContractRoutes
6+
import dev.typetype.server.routes.audioOnlySourceRoutes
7+
import dev.typetype.server.services.AudioOnlyMediaTokenService
8+
import dev.typetype.server.services.ProxyService
9+
import dev.typetype.server.services.PublicHlsManifestTokenService
10+
import dev.typetype.server.services.StreamService
11+
import io.ktor.client.request.get
12+
import io.ktor.client.request.header
13+
import io.ktor.client.statement.bodyAsText
14+
import io.ktor.http.HttpHeaders
15+
import io.ktor.http.HttpStatusCode
16+
import io.ktor.serialization.kotlinx.json.json
17+
import io.ktor.server.application.install
18+
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
19+
import io.ktor.server.routing.routing
20+
import io.ktor.server.testing.testApplication
21+
import io.mockk.coEvery
22+
import io.mockk.mockk
23+
import org.junit.jupiter.api.Assertions.assertEquals
24+
import org.junit.jupiter.api.Assertions.assertFalse
25+
import org.junit.jupiter.api.Assertions.assertTrue
26+
import org.junit.jupiter.api.Test
27+
import java.io.ByteArrayInputStream
28+
29+
class AudioOnlyPlayableContractRoutesTest {
30+
private val streamService: StreamService = mockk()
31+
private val proxyService: ProxyService = mockk()
32+
private val tokenService = AudioOnlyMediaTokenService("test-secret")
33+
private val hlsTokenService = PublicHlsManifestTokenService("test-secret")
34+
35+
@Test
36+
fun `GET audio-only skips progressive streams that cannot be proxied`() = testApplication {
37+
val blocked = testAudioStream(url = BLOCKED_AUDIO_URL, bitrate = 160, itag = 251)
38+
val playable = testAudioStream(url = PLAYABLE_AUDIO_URL, bitrate = 128, itag = 140)
39+
coEvery { streamService.getStreamInfo(any()) } returns ExtractionResult.Success(
40+
testStreamResponse(audioStreams = listOf(blocked, playable))
41+
)
42+
coEvery { proxyService.pipe(BLOCKED_AUDIO_URL, any(), any()) } returns ExtractionResult.Failure("Upstream returned 403")
43+
coEvery { proxyService.pipe(PLAYABLE_AUDIO_URL, any(), any()) } answers { playableAudioResponse() }
44+
installFullApp()
45+
46+
val contract = client.get("/streams/audio-only?url=https://youtube.com/watch?v=test")
47+
val source = client.get(contract.bodyAsText().extractSrc()) { header(HttpHeaders.Range, "bytes=0-") }
48+
49+
assertEquals(HttpStatusCode.OK, contract.status)
50+
assertTrue(contract.bodyAsText().contains("\"kind\":\"progressive\""))
51+
assertEquals(HttpStatusCode.PartialContent, source.status)
52+
assertEquals("audio/mp4", source.headers[HttpHeaders.ContentType]?.substringBefore(";"))
53+
}
54+
55+
@Test
56+
fun `GET audio-only falls back to signed HLS when progressive streams cannot be proxied`() = testApplication {
57+
val blocked = testAudioStream(url = BLOCKED_AUDIO_URL, bitrate = 160, itag = 251)
58+
coEvery { streamService.getStreamInfo(any()) } returns ExtractionResult.Success(
59+
testStreamResponse(audioStreams = listOf(blocked), hlsUrl = HLS_URL)
60+
)
61+
coEvery { proxyService.pipe(BLOCKED_AUDIO_URL, any(), any()) } returns ExtractionResult.Failure("Upstream returned 403")
62+
installFullApp()
63+
64+
val response = client.get("/streams/audio-only?url=https://youtube.com/watch?v=test")
65+
val body = response.bodyAsText()
66+
67+
assertEquals(HttpStatusCode.OK, response.status)
68+
assertTrue(body.contains("\"kind\":\"hls\""))
69+
assertTrue(body.contains("\"mimeType\":\"application/vnd.apple.mpegurl\""))
70+
assertTrue(body.contains("\"src\":\"/streams/hls-manifest?token="))
71+
assertFalse(body.contains("manifest.googlevideo.com"))
72+
}
73+
74+
private fun io.ktor.server.testing.ApplicationTestBuilder.installFullApp(): Unit = application {
75+
install(ContentNegotiation) { json() }
76+
routing {
77+
audioOnlyContractRoutes(streamService, tokenService, publicHlsManifestTokenService = hlsTokenService, proxyService = proxyService)
78+
audioOnlySourceRoutes(streamService, proxyService, tokenService)
79+
}
80+
}
81+
82+
private fun playableAudioResponse(): ExtractionResult.Success<ProxyResponse> = ExtractionResult.Success(
83+
ProxyResponse(
84+
status = 206,
85+
contentType = "audio/mp4",
86+
contentLength = 4,
87+
contentRange = "bytes 0-3/4",
88+
acceptRanges = "bytes",
89+
stream = ByteArrayInputStream(byteArrayOf(0, 1, 2, 3)),
90+
close = {},
91+
)
92+
)
93+
94+
private fun String.extractSrc(): String = Regex("\"src\":\"([^\"]+)\"").find(this)!!.groupValues[1]
95+
96+
private companion object {
97+
const val HLS_URL = "https://manifest.googlevideo.com/api/manifest/hls/test"
98+
const val BLOCKED_AUDIO_URL = "https://example.googlevideo.com/blocked-audio"
99+
const val PLAYABLE_AUDIO_URL = "https://example.googlevideo.com/playable-audio"
100+
}
101+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class AudioOnlyProgressiveContractRoutesTest {
6565
)
6666
)
6767
installSourceApp()
68-
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "en")
68+
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "en", 140, null)
6969

7070
val response = client.get("/streams/audio-only/source?token=$token") {
7171
header(HttpHeaders.Range, "bytes=0-1023")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class AudioOnlySourceOpenRangeRoutesTest {
4545
)
4646
)
4747
installSourceApp()
48-
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "en")
48+
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "en", 140, null)
4949

5050
val response = client.get("/streams/audio-only/source?token=$token") {
5151
header(HttpHeaders.Range, "bytes=0-")

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class AudioOnlySourceRoutesTest {
4646
)
4747
)
4848
installSourceApp()
49-
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "ja")
49+
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "ja", 140, null)
5050

5151
val response = client.get("/streams/audio-only/source?token=$token") {
5252
header(HttpHeaders.Range, "bytes=0-3")
@@ -76,7 +76,7 @@ class AudioOnlySourceRoutesTest {
7676
)
7777
)
7878
installSourceApp()
79-
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "en")
79+
val token = tokenService.createToken(null, "https://youtube.com/watch?v=test", false, "en", 140, null)
8080

8181
val response = client.get("/streams/audio-only/source?token=$token")
8282

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ fun testStreamResponse(
7474
videoOnlyStreams: List<VideoStreamItem> = listOf(testVideoStream()),
7575
audioStreams: List<AudioStreamItem> = listOf(testAudioStream()),
7676
duration: Long = 300,
77+
hlsUrl: String = "",
7778
dashMpdUrl: String = "",
7879
): StreamResponse = StreamResponse(
7980
id = "test-id",
@@ -104,7 +105,7 @@ fun testStreamResponse(
104105
requiresMembership = false,
105106
startPosition = 0L,
106107
streamSegments = emptyList(),
107-
hlsUrl = "",
108+
hlsUrl = hlsUrl,
108109
dashMpdUrl = dashMpdUrl,
109110
videoStreams = emptyList(),
110111
audioStreams = audioStreams,

0 commit comments

Comments
 (0)