Add downloading capabilities, not implemented yet

This commit is contained in:
Syer10
2021-06-11 18:58:01 -04:00
parent f516878a2b
commit 4725e12475
12 changed files with 211 additions and 2 deletions

View File

@@ -53,6 +53,7 @@ dependencies {
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("io.ktor:ktor-client-serialization:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-websockets:$ktorVersion")
// Logging
val log4jVersion = "2.14.1"

View File

@@ -8,6 +8,7 @@ package ca.gosyer.data
import ca.gosyer.core.prefs.PreferenceStoreFactory
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.reader.ReaderPreferences
@@ -73,4 +74,8 @@ val DataModule = module {
bind<ServerService>()
.toClass<ServerService>()
.singleton()
bind<DownloadService>()
.toClass<DownloadService>()
.singleton()
}

View File

@@ -0,0 +1,79 @@
/*
* 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.download
import ca.gosyer.BuildConfig
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 io.ktor.client.features.websocket.ws
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
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 serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
private val _downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped)
val downloaderStatus = _downloaderStatus.asStateFlow()
private val watching = mutableMapOf<Long, MutableSharedFlow<List<DownloadChapter>>>()
init {
serverUrl.mapLatest { serverUrl ->
while (true) {
runCatching {
client.ws(
host = serverUrl.substringAfter("://"),
path = downloadsQuery()
) {
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
val queue = status.queue.groupBy { it.mangaId }
watching.forEach { (mangaId, flow) ->
flow.emit(queue[mangaId].orEmpty())
}
}.throwIfCancellation()
}
}
}.throwIfCancellation()
}
}.launchIn(GlobalScope)
}
fun registerWatch(mangaId: Long) =
MutableSharedFlow<List<DownloadChapter>>().also { watching[mangaId] = it }.asSharedFlow()
fun removeWatch(mangaId: Long) {
watching.remove(mangaId)
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.download.model
import ca.gosyer.data.models.Chapter
import kotlinx.serialization.Serializable
@Serializable
data class DownloadChapter(
val chapterIndex: Int,
val mangaId: Long,
var state: DownloadState = DownloadState.Queued,
var progress: Float = 0f,
var tries: Int = 0,
var chapter: Chapter? = null,
)

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.download.model
import kotlinx.serialization.Serializable
@Serializable
enum class DownloadState(val state: Int) {
Queued(0),
Downloading(1),
Finished(2),
Error(3),
}

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.download.model
import kotlinx.serialization.Serializable
@Serializable
data class DownloadStatus(
val status: DownloaderStatus,
val queue: List<DownloadChapter>,
)

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.download.model
import kotlinx.serialization.Serializable
@Serializable
enum class DownloaderStatus {
Started,
Stopped
}

View File

@@ -13,6 +13,7 @@ import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logging
import io.ktor.client.features.websocket.WebSockets
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Provider
@@ -32,6 +33,7 @@ internal class HttpProvider @Inject constructor() : Provider<Http> {
}
)
}
install(WebSockets)
install(Logging) {
level = if (BuildConfig.DEBUG) {
LogLevel.HEADERS

View File

@@ -10,9 +10,11 @@ import ca.gosyer.data.models.Chapter
import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.deleteDownloadChapterRequest
import ca.gosyer.data.server.requests.getChapterQuery
import ca.gosyer.data.server.requests.getMangaChaptersQuery
import ca.gosyer.data.server.requests.getPageQuery
import ca.gosyer.data.server.requests.queueDownloadChapterRequest
import ca.gosyer.data.server.requests.updateChapterRequest
import ca.gosyer.util.lang.withIOContext
import io.ktor.client.request.HttpRequestBuilder
@@ -127,4 +129,28 @@ class ChapterInteractionHandler @Inject constructor(
suspend fun getPage(manga: Manga, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapterIndex, pageNum, block)
suspend fun getPage(manga: Manga, chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapter.index, pageNum, block)
suspend fun queueChapterDownload(mangaId: Long, chapterIndex: Int) = withIOContext {
client.getRepeat<HttpResponse>(
serverUrl + queueDownloadChapterRequest(mangaId, chapterIndex)
)
}
suspend fun queueChapterDownload(chapter: Chapter) = queueChapterDownload(chapter.mangaId, chapter.index)
suspend fun queueChapterDownload(manga: Manga, chapterIndex: Int) = queueChapterDownload(manga.id, chapterIndex)
suspend fun queueChapterDownload(manga: Manga, chapter: Chapter) = queueChapterDownload(manga.id, chapter.index)
suspend fun deleteChapterDownload(mangaId: Long, chapterIndex: Int) = withIOContext {
client.deleteRepeat<HttpResponse>(
serverUrl + deleteDownloadChapterRequest(mangaId, chapterIndex)
)
}
suspend fun deleteChapterDownload(chapter: Chapter) = deleteChapterDownload(chapter.mangaId, chapter.index)
suspend fun deleteChapterDownload(manga: Manga, chapterIndex: Int) = deleteChapterDownload(manga.id, chapterIndex)
suspend fun deleteChapterDownload(manga: Manga, chapter: Chapter) = deleteChapterDownload(manga.id, chapter.index)
}

View File

@@ -21,3 +21,11 @@ fun updateChapterRequest(mangaId: Long, chapterIndex: Int) =
@Get
fun getPageQuery(mangaId: Long, chapterIndex: Int, index: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex/page/$index"
@Get
fun queueDownloadChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/download/$mangaId/chapter/$chapterIndex"
@Delete
fun deleteDownloadChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/download/$mangaId/chapter/$chapterIndex"

View File

@@ -0,0 +1,19 @@
/*
* 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.server.requests
fun downloadsQuery() =
"/api/v1/downloads"
fun downloadsStartRequest() =
"/api/v1/downloads/start"
fun downloadsStopRequest() =
"/api/v1/downloads/stop"
fun downloadsClearRequest() =
"/api/v1/downloads/clear"

View File

@@ -63,7 +63,9 @@ suspend fun <T> withIOContext(
fun Throwable.throwIfCancellation() { if (this is CancellationException) throw this }
fun <T> Result<T>.throwIfCancellation(): Result<T> {
if (isFailure) {
val exception = exceptionOrNull()
if (exception is CancellationException) throw exception
}
return this
}