add SessionVideoApi

This commit is contained in:
2026-08-07 13:39:00 +03:00
parent b851cf5d6d
commit 23d9912449
10 changed files with 119 additions and 50 deletions
@@ -0,0 +1,81 @@
package ru.shadowsparky.vbox.backend.data.http
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.shadowsparky.vbox.shared.domain.VideoApi
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
import ru.shadowsparky.vbox.shared.domain.model.VideoLinksResponse
import ru.shadowsparky.vbox.shared.domain.model.VideosResponse
import java.util.Collections
import kotlin.time.Duration.Companion.days
class SessionVideoApi(private val wrapper: VideoApi) : VideoApi {
private val cacheTtlMs = 3.days.inWholeMilliseconds
private val maxCacheSize = 100
private val videosCache = createBoundedCache<String, VideosResponse>()
private val detailsCache = createBoundedCache<Long, VideoDetails>()
private val linksCache = createBoundedCache<VideoLinksKey, VideoLinksResponse>()
private val videosMutex = Mutex()
private val detailsMutex = Mutex()
private val linksMutex = Mutex()
override suspend fun fetchNewVideos(query: String?): VideosResponse {
val cacheKey = query ?: ""
videosCache.getValid(cacheKey)?.let { return it }
return videosMutex.withLock {
videosCache.getValid(cacheKey) ?: run {
val remoteData = wrapper.fetchNewVideos(query)
videosCache[cacheKey] = CacheEntry(remoteData)
remoteData
}
}
}
override suspend fun fetchDetails(id: Long): VideoDetails {
detailsCache.getValid(id)?.let { return it }
return detailsMutex.withLock {
detailsCache.getValid(id) ?: run {
val remoteData = wrapper.fetchDetails(id)
detailsCache[id] = CacheEntry(remoteData)
remoteData
}
}
}
override suspend fun fetchVideoLinks(id: Long, seasonId: Long?): VideoLinksResponse {
val cacheKey = VideoLinksKey(id, seasonId)
linksCache.getValid(cacheKey)?.let { return it }
return linksMutex.withLock {
linksCache.getValid(cacheKey) ?: run {
val remoteData = wrapper.fetchVideoLinks(id, seasonId)
linksCache[cacheKey] = CacheEntry(remoteData)
remoteData
}
}
}
private fun <K, V> MutableMap<K, CacheEntry<V>>.getValid(key: K): V? {
val entry = this[key] ?: return null
val isExpired = System.currentTimeMillis() - entry.createdAt > cacheTtlMs
return if (isExpired) {
this.remove(key)
null
} else {
entry.data
}
}
private fun <K, V> createBoundedCache(): MutableMap<K, CacheEntry<V>> {
val map = object : LinkedHashMap<K, CacheEntry<V>>(maxCacheSize + 1, 0.75f, false) {
override fun removeEldestEntry(eldest: Map.Entry<K, CacheEntry<V>>?): Boolean {
return size > maxCacheSize
}
}
return Collections.synchronizedMap(map)
}
private data class CacheEntry<T>(val data: T, val createdAt: Long = System.currentTimeMillis())
private data class VideoLinksKey(val id: Long, val seasonId: Long?)
}
@@ -3,11 +3,14 @@ package ru.shadowsparky.vbox.backend.di
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
import ru.shadowsparky.vbox.backend.data.http.ExternalBackendApi
import ru.shadowsparky.vbox.backend.data.http.SessionVideoApi
import ru.shadowsparky.vbox.shared.domain.VideoApi
@Module
class HttpModule {
@Factory
fun provideVideoApi(impl: ExternalBackendApi): VideoApi = impl
fun provideVideoApi(impl: ExternalBackendApi): VideoApi {
return SessionVideoApi(impl)
}
}
@@ -12,7 +12,6 @@ import ru.shadowsparky.http.domain.HttpException
import ru.shadowsparky.vbox.backend.di.AuthEntryPoint
import ru.shadowsparky.vbox.backend.di.RoutingEntryPoint
import ru.shadowsparky.vbox.backend.presentation.routing.setupAuthMethods
import ru.shadowsparky.vbox.backend.presentation.routing.setupBaseMethods
import ru.shadowsparky.vbox.shared.domain.model.ServerExceptionInfo
val routingLogger: Logger = LoggerFactory.getLogger("routing")
@@ -38,7 +37,6 @@ fun Application.configureRouting(
}
}
routing {
setupBaseMethods(routingEntryPoint.videoApi)
setupAuthMethods(routingEntryPoint, authEntryPoint)
}
}
@@ -28,22 +28,23 @@ fun Routing.setupAuthMethods(
post(AuthTokenRepository.UPDATE_PATH) {
call.respond(default.update(call.receive()))
}
post(AuthTokenRepository.REVOKE_PATH) {
default.revoke(call.receive())
call.respond(HttpStatusCode.OK)
}
authenticate(AUTH_JWT_NAME) {
get(HeathCheck.PATH) { call.respond(HttpStatusCode.OK) }
post(AuthTokenRepository.CHANGE_PASS_PATH) {
authEntryPoint.authTokenRepositoryFactory.create(call.obtainUserId())
.changePassword(call.receive())
call.respond(HttpStatusCode.OK)
}
setupBaseMethods(routingEntryPoint.videoApi)
setupSearch(searchFactory)
setupRecentlyWatched(recentlyFactory)
setupSavedMovie(savedMovieFactory)
setupTagsRouting(userTagFactory, movieTagFactory)
setupUpdates(updateFetcherFactory)
setupChat(chatRepositoryFactory, processUserMessageUseCase, remoteEventHandler)
get(HeathCheck.PATH) { call.respond(HttpStatusCode.OK) }
post(AuthTokenRepository.CHANGE_PASS_PATH) {
authEntryPoint.authTokenRepositoryFactory.create(call.obtainUserId())
.changePassword(call.receive())
call.respond(HttpStatusCode.OK)
}
post(AuthTokenRepository.REVOKE_PATH) {
default.revoke(call.receive())
call.respond(HttpStatusCode.OK)
}
}
}
@@ -1,13 +1,12 @@
package ru.shadowsparky.vbox.backend.presentation.routing
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respond
import io.ktor.server.routing.Routing
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.util.getOrFail
import ru.shadowsparky.vbox.shared.domain.VideoApi
fun Routing.setupBaseMethods(api: VideoApi) {
fun Route.setupBaseMethods(api: VideoApi) {
get(VideoApi.VIDEOS_PATH) {
val p = call.parameters
val obj = api.fetchNewVideos(p[VideoApi.SEARCH_ARG])
@@ -26,7 +25,4 @@ fun Routing.setupBaseMethods(api: VideoApi) {
)
)
}
get("/") {
call.respond(HttpStatusCode.NotFound)
}
}