mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2026-01-23 03:54:04 +01:00
Unimplemented library updates
This commit is contained in:
99
src/main/kotlin/ca/gosyer/core/service/WebsocketService.kt
Normal file
99
src/main/kotlin/ca/gosyer/core/service/WebsocketService.kt
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.core.service
|
||||
|
||||
import ca.gosyer.build.BuildConfig
|
||||
import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.util.lang.throwIfCancellation
|
||||
import ca.gosyer.util.system.CKLogger
|
||||
import io.ktor.client.features.websocket.ws
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
abstract class WebsocketService(
|
||||
protected val serverPreferences: ServerPreferences,
|
||||
protected val client: Http
|
||||
) {
|
||||
protected val json = Json {
|
||||
ignoreUnknownKeys = !BuildConfig.DEBUG
|
||||
}
|
||||
private val _status = MutableStateFlow(Status.STARTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
protected val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
|
||||
|
||||
private var errorConnectionCount = 0
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
||||
fun init() {
|
||||
errorConnectionCount = 0
|
||||
job?.cancel()
|
||||
job = serverUrl.mapLatest { serverUrl ->
|
||||
_status.value = Status.STARTING
|
||||
while (true) {
|
||||
if (errorConnectionCount > 3) {
|
||||
_status.value = Status.STOPPED
|
||||
throw CancellationException()
|
||||
}
|
||||
runCatching {
|
||||
client.ws(
|
||||
host = serverUrl.substringAfter("://"),
|
||||
path = query
|
||||
) {
|
||||
errorConnectionCount = 0
|
||||
_status.value = Status.RUNNING
|
||||
send(Frame.Text("STATUS"))
|
||||
|
||||
incoming.receiveAsFlow()
|
||||
.filterIsInstance<Frame.Text>()
|
||||
.mapLatest(::onReceived)
|
||||
.catch { it.throwIfCancellation() }
|
||||
.collect()
|
||||
}
|
||||
}.throwIfCancellation().isFailure.let {
|
||||
_status.value = Status.STARTING
|
||||
if (it) errorConnectionCount++
|
||||
}
|
||||
}
|
||||
}.catch {
|
||||
_status.value = Status.STOPPED
|
||||
error(it) { "Error while running websocket service" }
|
||||
throw it
|
||||
}.launchIn(GlobalScope)
|
||||
}
|
||||
|
||||
abstract val query: String
|
||||
|
||||
abstract suspend fun onReceived(frame: Frame.Text)
|
||||
|
||||
enum class Status {
|
||||
STARTING,
|
||||
RUNNING,
|
||||
STOPPED
|
||||
}
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import ca.gosyer.data.catalog.CatalogPreferences
|
||||
import ca.gosyer.data.download.DownloadService
|
||||
import ca.gosyer.data.extension.ExtensionPreferences
|
||||
import ca.gosyer.data.library.LibraryPreferences
|
||||
import ca.gosyer.data.library.LibraryUpdateService
|
||||
import ca.gosyer.data.reader.ReaderPreferences
|
||||
import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.HttpProvider
|
||||
@@ -100,4 +101,7 @@ val DataModule = module {
|
||||
bind<DownloadService>()
|
||||
.toClass<DownloadService>()
|
||||
.singleton()
|
||||
bind<LibraryUpdateService>()
|
||||
.toClass<LibraryUpdateService>()
|
||||
.singleton()
|
||||
}
|
||||
|
||||
@@ -6,45 +6,30 @@
|
||||
|
||||
package ca.gosyer.data.download
|
||||
|
||||
import ca.gosyer.build.BuildConfig
|
||||
import ca.gosyer.core.service.WebsocketService
|
||||
import ca.gosyer.data.download.model.DownloadChapter
|
||||
import ca.gosyer.data.download.model.DownloadStatus
|
||||
import ca.gosyer.data.download.model.DownloaderStatus
|
||||
import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.requests.downloadsQuery
|
||||
import ca.gosyer.util.lang.throwIfCancellation
|
||||
import ca.gosyer.util.system.CKLogger
|
||||
import io.ktor.client.features.websocket.ws
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
import io.ktor.http.cio.websocket.readText
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class DownloadService @Inject constructor(
|
||||
val serverPreferences: ServerPreferences,
|
||||
val client: Http
|
||||
) {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = !BuildConfig.DEBUG
|
||||
}
|
||||
private val _status = MutableStateFlow(Status.STARTING)
|
||||
val status = _status.asStateFlow()
|
||||
serverPreferences: ServerPreferences,
|
||||
client: Http
|
||||
) : WebsocketService(serverPreferences, client) {
|
||||
|
||||
private val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
|
||||
private val _downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped)
|
||||
val downloaderStatus = _downloaderStatus.asStateFlow()
|
||||
|
||||
@@ -52,56 +37,18 @@ class DownloadService @Inject constructor(
|
||||
val downloadQueue = _downloadQueue.asStateFlow()
|
||||
|
||||
private val watching = mutableMapOf<Long, MutableSharedFlow<Pair<Long, List<DownloadChapter>>>>()
|
||||
private var errorConnectionCount = 0
|
||||
|
||||
private var job: Job? = null
|
||||
init {
|
||||
init()
|
||||
}
|
||||
override val query: String
|
||||
get() = downloadsQuery()
|
||||
|
||||
fun init() {
|
||||
errorConnectionCount = 0
|
||||
job?.cancel()
|
||||
job = serverUrl.mapLatest { serverUrl ->
|
||||
_status.value = Status.STARTING
|
||||
while (true) {
|
||||
if (errorConnectionCount > 3) {
|
||||
_status.value = Status.STOPPED
|
||||
throw CancellationException()
|
||||
}
|
||||
runCatching {
|
||||
client.ws(
|
||||
host = serverUrl.substringAfter("://"),
|
||||
path = downloadsQuery()
|
||||
) {
|
||||
errorConnectionCount = 0
|
||||
_status.value = Status.RUNNING
|
||||
send(Frame.Text("STATUS"))
|
||||
|
||||
while (true) {
|
||||
val frame = incoming.receive()
|
||||
runCatching {
|
||||
frame as Frame.Text
|
||||
val status = json.decodeFromString<DownloadStatus>(frame.readText())
|
||||
_downloaderStatus.value = status.status
|
||||
_downloadQueue.value = status.queue
|
||||
val queue = status.queue.groupBy { it.mangaId }
|
||||
watching.forEach { (mangaId, flow) ->
|
||||
flow.emit(mangaId to queue[mangaId].orEmpty())
|
||||
}
|
||||
}.throwIfCancellation()
|
||||
}
|
||||
}
|
||||
}.throwIfCancellation().isFailure.let {
|
||||
_status.value = Status.STARTING
|
||||
if (it) errorConnectionCount++
|
||||
}
|
||||
}
|
||||
}.catch {
|
||||
_status.value = Status.STOPPED
|
||||
error(it) { "Error while running downloader" }
|
||||
throw it
|
||||
}.launchIn(GlobalScope)
|
||||
override suspend fun onReceived(frame: Frame.Text) {
|
||||
val status = json.decodeFromString<DownloadStatus>(frame.readText())
|
||||
_downloaderStatus.value = status.status
|
||||
_downloadQueue.value = status.queue
|
||||
val queue = status.queue.groupBy { it.mangaId }
|
||||
watching.forEach { (mangaId, flow) ->
|
||||
flow.emit(mangaId to queue[mangaId].orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun registerWatch(mangaId: Long) =
|
||||
@@ -116,11 +63,5 @@ class DownloadService @Inject constructor(
|
||||
watching -= mangaIds
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
STARTING,
|
||||
RUNNING,
|
||||
STOPPED
|
||||
}
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.library
|
||||
|
||||
import ca.gosyer.core.service.WebsocketService
|
||||
import ca.gosyer.data.library.model.UpdateStatus
|
||||
import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.requests.updatesQuery
|
||||
import ca.gosyer.util.system.CKLogger
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
import io.ktor.http.cio.websocket.readText
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class LibraryUpdateService @Inject constructor(
|
||||
serverPreferences: ServerPreferences,
|
||||
client: Http
|
||||
) : WebsocketService(serverPreferences, client) {
|
||||
|
||||
override val query: String
|
||||
get() = updatesQuery()
|
||||
|
||||
override suspend fun onReceived(frame: Frame.Text) {
|
||||
val status = json.decodeFromString<UpdateStatus>(frame.readText())
|
||||
info { status }
|
||||
}
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
17
src/main/kotlin/ca/gosyer/data/library/model/JobStatus.kt
Normal file
17
src/main/kotlin/ca/gosyer/data/library/model/JobStatus.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.library.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class JobStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
COMPLETE,
|
||||
FAILED
|
||||
}
|
||||
15
src/main/kotlin/ca/gosyer/data/library/model/UpdateStatus.kt
Normal file
15
src/main/kotlin/ca/gosyer/data/library/model/UpdateStatus.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.library.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UpdateStatus(
|
||||
val statusMap: Map<JobStatus, Int>,
|
||||
val running: Boolean
|
||||
)
|
||||
@@ -6,12 +6,18 @@
|
||||
|
||||
package ca.gosyer.data.server.interactions
|
||||
|
||||
import ca.gosyer.data.models.Category
|
||||
import ca.gosyer.data.models.Updates
|
||||
import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.requests.fetchUpdatesRequest
|
||||
import ca.gosyer.data.server.requests.recentUpdatesQuery
|
||||
import ca.gosyer.util.lang.withIOContext
|
||||
import io.ktor.client.request.forms.submitForm
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.Parameters
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpdatesInteractionHandler @Inject constructor(
|
||||
@@ -24,4 +30,21 @@ class UpdatesInteractionHandler @Inject constructor(
|
||||
serverUrl + recentUpdatesQuery(pageNum)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateLibrary() = withIOContext {
|
||||
client.post<HttpResponse>(
|
||||
serverUrl + fetchUpdatesRequest()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateCategory(categoryId: Long) = withIOContext {
|
||||
client.submitForm<HttpResponse>(
|
||||
serverUrl + fetchUpdatesRequest(),
|
||||
formParameters = Parameters.build {
|
||||
append("category", categoryId.toString())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateCategory(category: Category) = updateCategory(category.id)
|
||||
}
|
||||
|
||||
@@ -9,3 +9,15 @@ package ca.gosyer.data.server.requests
|
||||
@Get
|
||||
fun recentUpdatesQuery(pageNum: Int) =
|
||||
"/api/v1/update/recentChapters/$pageNum"
|
||||
|
||||
@Post
|
||||
fun fetchUpdatesRequest() =
|
||||
"/api/v1/update/fetch"
|
||||
|
||||
@Get
|
||||
fun updatesSummaryQuery() =
|
||||
"/api/v1/update/summary"
|
||||
|
||||
@WS
|
||||
fun updatesQuery() =
|
||||
"/api/v1/update"
|
||||
|
||||
@@ -11,6 +11,7 @@ import ca.gosyer.data.models.Category
|
||||
import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.UpdatesInteractionHandler
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import ca.gosyer.util.compose.saveIntInBundle
|
||||
import ca.gosyer.util.compose.saveStringInBundle
|
||||
@@ -72,6 +73,7 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
private val bundle: Bundle,
|
||||
private val categoryHandler: CategoryInteractionHandler,
|
||||
private val libraryHandler: LibraryInteractionHandler,
|
||||
private val updatesHandler: UpdatesInteractionHandler,
|
||||
libraryPreferences: LibraryPreferences
|
||||
) : ViewModel() {
|
||||
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
|
||||
@@ -155,6 +157,18 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
fun updateLibrary() {
|
||||
scope.launch {
|
||||
updatesHandler.updateLibrary()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(category: Category) {
|
||||
scope.launch {
|
||||
updatesHandler.updateCategory(category)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUERY_KEY = "query"
|
||||
const val SELECTED_CATEGORY_KEY = "selected_category"
|
||||
|
||||
@@ -17,7 +17,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.download.DownloadService
|
||||
import ca.gosyer.core.service.WebsocketService
|
||||
import ca.gosyer.ui.base.resources.stringResource
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.downloads.DownloadsMenuViewModel
|
||||
@@ -28,13 +28,13 @@ fun DownloadsExtraInfo() {
|
||||
val status by vm.serviceStatus.collectAsState()
|
||||
val list by vm.downloadQueue.collectAsState()
|
||||
val text = when (status) {
|
||||
DownloadService.Status.STARTING -> stringResource("downloads_loading")
|
||||
DownloadService.Status.RUNNING -> {
|
||||
WebsocketService.Status.STARTING -> stringResource("downloads_loading")
|
||||
WebsocketService.Status.RUNNING -> {
|
||||
if (list.isNotEmpty()) {
|
||||
stringResource("downloads_remaining", list.size)
|
||||
} else null
|
||||
}
|
||||
DownloadService.Status.STOPPED -> null
|
||||
WebsocketService.Status.STOPPED -> null
|
||||
}
|
||||
if (text != null) {
|
||||
Text(
|
||||
@@ -42,7 +42,7 @@ fun DownloadsExtraInfo() {
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
|
||||
)
|
||||
} else if (status == DownloadService.Status.STOPPED) {
|
||||
} else if (status == WebsocketService.Status.STOPPED) {
|
||||
Surface(onClick = vm::restartDownloader, shape = RoundedCornerShape(4.dp)) {
|
||||
Text(
|
||||
stringResource("downloads_stopped"),
|
||||
|
||||
Reference in New Issue
Block a user