11package dev.typetype.server.services
22
3+ import dev.typetype.server.cache.CacheService
34import dev.typetype.server.models.ExtractionResult
45import dev.typetype.server.models.StreamResponse
6+ import kotlinx.coroutines.CompletableDeferred
57import kotlinx.coroutines.Dispatchers
68import kotlinx.coroutines.withContext
79import okhttp3.OkHttpClient
810import okhttp3.Request
911import java.net.URLEncoder
1012import java.nio.charset.StandardCharsets
13+ import java.util.concurrent.ConcurrentHashMap
1114
1215internal fun isManifestUrl (url : String ): Boolean {
1316 if (! url.startsWith(" http" )) return false
@@ -38,7 +41,10 @@ private fun toHlsProxyUrl(url: String): String {
3841class HlsManifestService (
3942 private val streamService : StreamService ,
4043 private val httpClient : OkHttpClient ,
44+ cache : CacheService ? = null ,
4145) {
46+ private val manifestCache = cache?.let (::HlsManifestCache )
47+ private val inFlight = ConcurrentHashMap <String , CompletableDeferred <ExtractionResult <String >>>()
4248
4349 suspend fun hlsManifest (url : String ): ExtractionResult <String > {
4450 val manifestUrl = if (isManifestUrl(url)) {
@@ -50,7 +56,7 @@ class HlsManifestService(
5056 is ExtractionResult .Failure -> return resolved
5157 }
5258 }
53- return fetchAndRewrite (manifestUrl)
59+ return cachedOrFetch (manifestUrl)
5460 }
5561
5662 suspend fun hlsManifestFromStreamInfo (result : ExtractionResult <StreamResponse >): ExtractionResult <String > {
@@ -59,7 +65,30 @@ class HlsManifestService(
5965 is ExtractionResult .BadRequest -> return resolved
6066 is ExtractionResult .Failure -> return resolved
6167 }
62- return fetchAndRewrite(manifestUrl)
68+ return cachedOrFetch(manifestUrl)
69+ }
70+
71+ private suspend fun cachedOrFetch (manifestUrl : String ): ExtractionResult <String > {
72+ manifestCache?.get(manifestUrl)?.let { return ExtractionResult .Success (it) }
73+ val pending = CompletableDeferred <ExtractionResult <String >>()
74+ val existing = inFlight.putIfAbsent(manifestUrl, pending)
75+ if (existing != null ) return existing.await()
76+ return try {
77+ manifestCache?.get(manifestUrl)?.let {
78+ val result = ExtractionResult .Success (it)
79+ pending.complete(result)
80+ return result
81+ }
82+ val result = fetchAndRewrite(manifestUrl)
83+ if (result is ExtractionResult .Success ) manifestCache?.set(manifestUrl, result.data)
84+ pending.complete(result)
85+ result
86+ } catch (error: Throwable ) {
87+ pending.completeExceptionally(error)
88+ throw error
89+ } finally {
90+ inFlight.remove(manifestUrl, pending)
91+ }
6392 }
6493
6594 private suspend fun resolveHlsUrl (videoUrl : String ): ExtractionResult <String > {
0 commit comments