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.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)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-10
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
-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.offline_video
|
||||
import androiddev.apps.vbox.client.shared.generated.resources.user_tags
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -44,9 +43,9 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
||||
if (servAddr != null && comp.isDebug) {
|
||||
item {
|
||||
TextPreference(
|
||||
modifier = Modifier.clickable { comp.editServAddr() },
|
||||
stringResource(Res.string.custom_serv_addr),
|
||||
servAddr!!
|
||||
{ comp.editServAddr() },
|
||||
summary = servAddr!!
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -64,21 +63,21 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
||||
item {
|
||||
TextPreference(
|
||||
text = stringResource(Res.string.offline_video),
|
||||
modifier = Modifier.clickable { comp.showOffline() }
|
||||
onClick = { comp.showOffline() }
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
TextPreference(
|
||||
text = stringResource(Res.string.user_tags),
|
||||
modifier = Modifier.clickable { comp.showTags() }
|
||||
onClick = { comp.showTags() }
|
||||
)
|
||||
}
|
||||
if (comp.updateComponent.hasSupport) {
|
||||
item {
|
||||
TextPreference(
|
||||
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
|
||||
}
|
||||
TextPreference(
|
||||
modifier = Modifier.clickable {
|
||||
if (needAuth) comp.doAuth() else comp.logout()
|
||||
},
|
||||
stringResource(msg)
|
||||
stringResource(msg),
|
||||
{ if (needAuth) comp.doAuth() else comp.logout() }
|
||||
)
|
||||
}
|
||||
if (needAuth) {
|
||||
item {
|
||||
TextPreference(
|
||||
modifier = Modifier.clickable { comp.doRegister() },
|
||||
stringResource(Res.string.account_reg)
|
||||
stringResource(Res.string.account_reg),
|
||||
{ comp.doRegister() }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
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_save
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -66,7 +65,7 @@ fun TagScreen(component: TagComponent) {
|
||||
) { tag ->
|
||||
TextPreference(
|
||||
text = tag.tag,
|
||||
modifier = Modifier.clickable { activeTag = tag }
|
||||
{ activeTag = tag }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package ru.shadowsparky.http.domain
|
||||
|
||||
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)
|
||||
open class NotFoundException(msg: String) : HttpException(HttpStatusCode.NotFound.value, msg)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ru.shadowsparky.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -10,16 +11,19 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun TextPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
summary: String? = null
|
||||
) {
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(Color.Transparent),
|
||||
onClick = onClick,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
headlineContent = {
|
||||
content = {
|
||||
Text(
|
||||
text,
|
||||
maxLines = 1,
|
||||
|
||||
Reference in New Issue
Block a user