Unimplemented library updates

This commit is contained in:
Syer10
2021-11-29 10:43:54 -05:00
parent 4f7611cbec
commit 89622ad3d0
10 changed files with 239 additions and 78 deletions

View 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({})
}

View File

@@ -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()
}

View File

@@ -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({})
}

View File

@@ -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({})
}

View 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
}

View 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
)

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"),