add SessionVideoApi
This commit is contained in:
+81
@@ -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.Factory
|
||||||
import org.koin.core.annotation.Module
|
import org.koin.core.annotation.Module
|
||||||
import ru.shadowsparky.vbox.backend.data.http.ExternalBackendApi
|
import ru.shadowsparky.vbox.backend.data.http.ExternalBackendApi
|
||||||
|
import ru.shadowsparky.vbox.backend.data.http.SessionVideoApi
|
||||||
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class HttpModule {
|
class HttpModule {
|
||||||
|
|
||||||
@Factory
|
@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.AuthEntryPoint
|
||||||
import ru.shadowsparky.vbox.backend.di.RoutingEntryPoint
|
import ru.shadowsparky.vbox.backend.di.RoutingEntryPoint
|
||||||
import ru.shadowsparky.vbox.backend.presentation.routing.setupAuthMethods
|
import ru.shadowsparky.vbox.backend.presentation.routing.setupAuthMethods
|
||||||
import ru.shadowsparky.vbox.backend.presentation.routing.setupBaseMethods
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.model.ServerExceptionInfo
|
import ru.shadowsparky.vbox.shared.domain.model.ServerExceptionInfo
|
||||||
|
|
||||||
val routingLogger: Logger = LoggerFactory.getLogger("routing")
|
val routingLogger: Logger = LoggerFactory.getLogger("routing")
|
||||||
@@ -38,7 +37,6 @@ fun Application.configureRouting(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
setupBaseMethods(routingEntryPoint.videoApi)
|
|
||||||
setupAuthMethods(routingEntryPoint, authEntryPoint)
|
setupAuthMethods(routingEntryPoint, authEntryPoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -28,22 +28,23 @@ fun Routing.setupAuthMethods(
|
|||||||
post(AuthTokenRepository.UPDATE_PATH) {
|
post(AuthTokenRepository.UPDATE_PATH) {
|
||||||
call.respond(default.update(call.receive()))
|
call.respond(default.update(call.receive()))
|
||||||
}
|
}
|
||||||
post(AuthTokenRepository.REVOKE_PATH) {
|
|
||||||
default.revoke(call.receive())
|
|
||||||
call.respond(HttpStatusCode.OK)
|
|
||||||
}
|
|
||||||
authenticate(AUTH_JWT_NAME) {
|
authenticate(AUTH_JWT_NAME) {
|
||||||
get(HeathCheck.PATH) { call.respond(HttpStatusCode.OK) }
|
setupBaseMethods(routingEntryPoint.videoApi)
|
||||||
post(AuthTokenRepository.CHANGE_PASS_PATH) {
|
|
||||||
authEntryPoint.authTokenRepositoryFactory.create(call.obtainUserId())
|
|
||||||
.changePassword(call.receive())
|
|
||||||
call.respond(HttpStatusCode.OK)
|
|
||||||
}
|
|
||||||
setupSearch(searchFactory)
|
setupSearch(searchFactory)
|
||||||
setupRecentlyWatched(recentlyFactory)
|
setupRecentlyWatched(recentlyFactory)
|
||||||
setupSavedMovie(savedMovieFactory)
|
setupSavedMovie(savedMovieFactory)
|
||||||
setupTagsRouting(userTagFactory, movieTagFactory)
|
setupTagsRouting(userTagFactory, movieTagFactory)
|
||||||
setupUpdates(updateFetcherFactory)
|
setupUpdates(updateFetcherFactory)
|
||||||
setupChat(chatRepositoryFactory, processUserMessageUseCase, remoteEventHandler)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-6
@@ -1,13 +1,12 @@
|
|||||||
package ru.shadowsparky.vbox.backend.presentation.routing
|
package ru.shadowsparky.vbox.backend.presentation.routing
|
||||||
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.server.response.respond
|
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.routing.get
|
||||||
import io.ktor.server.util.getOrFail
|
import io.ktor.server.util.getOrFail
|
||||||
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
||||||
|
|
||||||
fun Routing.setupBaseMethods(api: VideoApi) {
|
fun Route.setupBaseMethods(api: VideoApi) {
|
||||||
get(VideoApi.VIDEOS_PATH) {
|
get(VideoApi.VIDEOS_PATH) {
|
||||||
val p = call.parameters
|
val p = call.parameters
|
||||||
val obj = api.fetchNewVideos(p[VideoApi.SEARCH_ARG])
|
val obj = api.fetchNewVideos(p[VideoApi.SEARCH_ARG])
|
||||||
@@ -26,7 +25,4 @@ fun Routing.setupBaseMethods(api: VideoApi) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
get("/") {
|
|
||||||
call.respond(HttpStatusCode.NotFound)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package ru.shadowsparky.videobox.multiplatform.preview
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import ru.shadowsparky.ui.components.TextPreference
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun SwitchPreferencePreview() {
|
|
||||||
TextPreference(Modifier, "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", "summary")
|
|
||||||
}
|
|
||||||
+11
-14
@@ -10,7 +10,6 @@ import androiddev.apps.vbox.client.shared.generated.resources.edit_password
|
|||||||
import androiddev.apps.vbox.client.shared.generated.resources.external_player_title
|
import androiddev.apps.vbox.client.shared.generated.resources.external_player_title
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.offline_video
|
import androiddev.apps.vbox.client.shared.generated.resources.offline_video
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.user_tags
|
import androiddev.apps.vbox.client.shared.generated.resources.user_tags
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -44,9 +43,9 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
|||||||
if (servAddr != null && comp.isDebug) {
|
if (servAddr != null && comp.isDebug) {
|
||||||
item {
|
item {
|
||||||
TextPreference(
|
TextPreference(
|
||||||
modifier = Modifier.clickable { comp.editServAddr() },
|
|
||||||
stringResource(Res.string.custom_serv_addr),
|
stringResource(Res.string.custom_serv_addr),
|
||||||
servAddr!!
|
{ comp.editServAddr() },
|
||||||
|
summary = servAddr!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,21 +63,21 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
|||||||
item {
|
item {
|
||||||
TextPreference(
|
TextPreference(
|
||||||
text = stringResource(Res.string.offline_video),
|
text = stringResource(Res.string.offline_video),
|
||||||
modifier = Modifier.clickable { comp.showOffline() }
|
onClick = { comp.showOffline() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
TextPreference(
|
TextPreference(
|
||||||
text = stringResource(Res.string.user_tags),
|
text = stringResource(Res.string.user_tags),
|
||||||
modifier = Modifier.clickable { comp.showTags() }
|
onClick = { comp.showTags() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (comp.updateComponent.hasSupport) {
|
if (comp.updateComponent.hasSupport) {
|
||||||
item {
|
item {
|
||||||
TextPreference(
|
TextPreference(
|
||||||
text = stringResource(Res.string.check_for_updates),
|
text = stringResource(Res.string.check_for_updates),
|
||||||
modifier = Modifier.clickable { comp.checkUpdates() }
|
onClick = { comp.checkUpdates() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,24 +91,22 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
|||||||
Res.string.account_logout_auth
|
Res.string.account_logout_auth
|
||||||
}
|
}
|
||||||
TextPreference(
|
TextPreference(
|
||||||
modifier = Modifier.clickable {
|
stringResource(msg),
|
||||||
if (needAuth) comp.doAuth() else comp.logout()
|
{ if (needAuth) comp.doAuth() else comp.logout() }
|
||||||
},
|
|
||||||
stringResource(msg)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (needAuth) {
|
if (needAuth) {
|
||||||
item {
|
item {
|
||||||
TextPreference(
|
TextPreference(
|
||||||
modifier = Modifier.clickable { comp.doRegister() },
|
stringResource(Res.string.account_reg),
|
||||||
stringResource(Res.string.account_reg)
|
{ comp.doRegister() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item {
|
item {
|
||||||
TextPreference(
|
TextPreference(
|
||||||
modifier = Modifier.clickable { comp.editPassword() },
|
stringResource(Res.string.edit_password),
|
||||||
stringResource(Res.string.edit_password)
|
{ comp.editPassword() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -8,7 +8,6 @@ import androiddev.apps.vbox.client.shared.generated.resources.user_tags_new_tag
|
|||||||
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_new_tag_save
|
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_new_tag_save
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_save
|
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_save
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_tag_delete
|
import androiddev.apps.vbox.client.shared.generated.resources.user_tags_tag_delete
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -66,7 +65,7 @@ fun TagScreen(component: TagComponent) {
|
|||||||
) { tag ->
|
) { tag ->
|
||||||
TextPreference(
|
TextPreference(
|
||||||
text = tag.tag,
|
text = tag.tag,
|
||||||
modifier = Modifier.clickable { activeTag = tag }
|
{ activeTag = tag }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package ru.shadowsparky.http.domain
|
|||||||
|
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
|
|
||||||
open class HttpException(val httpCode: Int, override val message: String?) : RuntimeException()
|
open class HttpException(val httpCode: Int, override val message: String?) : RuntimeException() {
|
||||||
|
constructor(code: HttpStatusCode) : this(code.value, code.description)
|
||||||
|
}
|
||||||
|
|
||||||
class BadRequestException(msg: String) : HttpException(HttpStatusCode.BadRequest.value, msg)
|
class BadRequestException(msg: String) : HttpException(HttpStatusCode.BadRequest.value, msg)
|
||||||
open class NotFoundException(msg: String) : HttpException(HttpStatusCode.NotFound.value, msg)
|
open class NotFoundException(msg: String) : HttpException(HttpStatusCode.NotFound.value, msg)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.shadowsparky.ui.components
|
package ru.shadowsparky.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -10,16 +11,19 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TextPreference(
|
fun TextPreference(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
text: String,
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
summary: String? = null
|
summary: String? = null
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
colors = ListItemDefaults.colors(Color.Transparent),
|
colors = ListItemDefaults.colors(Color.Transparent),
|
||||||
|
onClick = onClick,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
headlineContent = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user