From 769472b24ce16001a126671abb37381092a975d2 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Thu, 1 Apr 2021 16:07:35 -0400 Subject: [PATCH] Implement coroutines --- server/build.gradle.kts | 12 +- .../kotlin/ir/armor/tachidesk/impl/Chapter.kt | 48 ++++--- .../ir/armor/tachidesk/impl/Extension.kt | 130 +++++++++--------- .../ir/armor/tachidesk/impl/ExtensionsList.kt | 10 +- .../kotlin/ir/armor/tachidesk/impl/Library.kt | 4 +- .../kotlin/ir/armor/tachidesk/impl/Manga.kt | 10 +- .../ir/armor/tachidesk/impl/MangaList.kt | 9 +- .../kotlin/ir/armor/tachidesk/impl/Page.kt | 12 +- .../kotlin/ir/armor/tachidesk/impl/Search.kt | 5 +- .../ir/armor/tachidesk/impl/util/File.kt | 2 +- .../ir/armor/tachidesk/server/JavalinSetup.kt | 41 ++++-- .../kotlin/ir/armor/tachidesk/util/OkHttp.kt | 41 ++++++ .../armor/tachidesk/util/RxCoroutineBridge.kt | 60 ++++++++ 13 files changed, 260 insertions(+), 124 deletions(-) create mode 100644 server/src/main/kotlin/ir/armor/tachidesk/util/OkHttp.kt create mode 100644 server/src/main/kotlin/ir/armor/tachidesk/util/RxCoroutineBridge.kt diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 11adada..8cfe4c3 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { val coroutinesVersion = "1.3.9" implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") // dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon") @@ -135,6 +136,15 @@ tasks { archiveVersion.set(TachideskVersion) archiveClassifier.set(TachideskRevision) } + withType { + kotlinOptions { + freeCompilerArgs = listOf( + "-Xopt-in=kotlin.RequiresOptIn", + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi" + ) + } + } } launch4j { //used for windows @@ -195,7 +205,7 @@ tasks.register("downloadJre") { } tasks.withType { - destinationDir = File("$rootDir/server/build") + destinationDirectory.set(File("$rootDir/server/build")) dependsOn("formatKotlin", "lintKotlin") } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt index 6794d4b..cba128e 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt @@ -9,12 +9,15 @@ package ir.armor.tachidesk.impl import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.lang.awaitSingle import ir.armor.tachidesk.impl.Manga.getManga import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.model.database.ChapterTable import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.database.PageTable import ir.armor.tachidesk.model.dataclass.ChapterDataClass +import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insertAndGetId @@ -24,7 +27,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update object Chapter { - fun getChapterList(mangaId: Int): List { + suspend fun getChapterList(mangaId: Int): List { val mangaDetails = getManga(mangaId) val source = getHttpSource(mangaDetails.sourceId.toLong()) @@ -33,7 +36,7 @@ object Chapter { title = mangaDetails.title url = mangaDetails.url } - ).toBlocking().first() + ).awaitSingle() val chapterCount = chapterList.count() @@ -86,33 +89,40 @@ object Chapter { } } - fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass { - return transaction { - val chapterEntry = ChapterTable.select { + suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass { + var chapterEntry: ResultRow? = null + var source: HttpSource? = null + var sChapter: SChapter? = null + transaction { + chapterEntry = ChapterTable.select { ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId) }.firstOrNull()!! val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! - val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) + source = getHttpSource(mangaEntry[MangaTable.sourceReference]) + sChapter = SChapter.create().apply { + url = chapterEntry!![ChapterTable.url] + name = chapterEntry!![ChapterTable.name] + } + } + val pageList = source!!.fetchPageList( + sChapter!! + ).awaitSingle() - val pageList = source.fetchPageList( - SChapter.create().apply { - url = chapterEntry[ChapterTable.url] - name = chapterEntry[ChapterTable.name] - } - ).toBlocking().first() + return transaction { + val chapterRow = chapterEntry!! - val chapterId = chapterEntry[ChapterTable.id].value + val chapterId = chapterRow[ChapterTable.id].value val chapterCount = transaction { ChapterTable.selectAll().count() } val chapter = ChapterDataClass( chapterId, - chapterEntry[ChapterTable.url], - chapterEntry[ChapterTable.name], - chapterEntry[ChapterTable.date_upload], - chapterEntry[ChapterTable.chapter_number], - chapterEntry[ChapterTable.scanlator], + chapterRow[ChapterTable.url], + chapterRow[ChapterTable.name], + chapterRow[ChapterTable.date_upload], + chapterRow[ChapterTable.chapter_number], + chapterRow[ChapterTable.scanlator], mangaId, - chapterEntry[ChapterTable.chapterIndex], + chapterRow[ChapterTable.chapterIndex], chapterCount.toInt(), pageList.count() diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt index 160214d..bb0147c 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt @@ -20,7 +20,7 @@ import ir.armor.tachidesk.impl.util.APKExtractor import ir.armor.tachidesk.model.database.ExtensionTable import ir.armor.tachidesk.model.database.SourceTable import ir.armor.tachidesk.server.ApplicationDirs -import kotlinx.coroutines.runBlocking +import ir.armor.tachidesk.util.await import mu.KotlinLogging import okhttp3.Request import okio.buffer @@ -86,7 +86,7 @@ object Extension { return classToLoad.getDeclaredConstructor().newInstance() } - fun installExtension(pkgName: String): Int { + suspend fun installExtension(pkgName: String): Int { logger.debug("Installing $pkgName") val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName } val fileNameWithoutType = extensionRecord.apkName.substringBefore(".apk") @@ -95,74 +95,72 @@ object Extension { // check if we don't have the dex file already downloaded val jarPath = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType.jar" if (!File(jarPath).exists()) { - runBlocking { - val apkToDownload = ExtensionGithubApi.getApkUrl(extensionRecord) + val apkToDownload = ExtensionGithubApi.getApkUrl(extensionRecord) - val apkFilePath = "$dirPathWithoutType.apk" - val jarFilePath = "$dirPathWithoutType.jar" - val dexFilePath = "$dirPathWithoutType.dex" + val apkFilePath = "$dirPathWithoutType.apk" + val jarFilePath = "$dirPathWithoutType.jar" + val dexFilePath = "$dirPathWithoutType.dex" - // download apk file - downloadAPKFile(apkToDownload, apkFilePath) + // download apk file + downloadAPKFile(apkToDownload, apkFilePath) - val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath) - logger.debug(className) - // dex -> jar - dex2jar(dexFilePath, jarFilePath, fileNameWithoutType) + val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath) + logger.debug(className) + // dex -> jar + dex2jar(dexFilePath, jarFilePath, fileNameWithoutType) - // clean up - File(apkFilePath).delete() - File(dexFilePath).delete() + // clean up + File(apkFilePath).delete() + File(dexFilePath).delete() - // update sources of the extension - val instance = loadExtensionInstance(jarFilePath, className) + // update sources of the extension + val instance = loadExtensionInstance(jarFilePath, className) - val extensionId = transaction { - ExtensionTable.select { ExtensionTable.pkgName eq extensionRecord.pkgName }.firstOrNull()!![ExtensionTable.id] + val extensionId = transaction { + ExtensionTable.select { ExtensionTable.pkgName eq extensionRecord.pkgName }.firstOrNull()!![ExtensionTable.id] + } + + when (instance) { + is HttpSource -> { // single source + transaction { + if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) { + SourceTable.insert { + it[this.id] = instance.id + it[name] = instance.name + it[this.lang] = instance.lang + it[extension] = extensionId + } + } + logger.debug("Installed source ${instance.name} with id ${instance.id}") + } } - - when (instance) { - is HttpSource -> { // single source - transaction { - if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) { + is SourceFactory -> { // theme source or multi lang + transaction { + instance.createSources().forEachIndexed { index, source -> + val httpSource = source as HttpSource + if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) { SourceTable.insert { - it[this.id] = instance.id - it[name] = instance.name - it[this.lang] = instance.lang + it[this.id] = httpSource.id + it[name] = httpSource.name + it[this.lang] = httpSource.lang it[extension] = extensionId + it[partOfFactorySource] = true } } - logger.debug("Installed source ${instance.name} with id ${instance.id}") + logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}") } } - is SourceFactory -> { // theme source or multi lang - transaction { - instance.createSources().forEachIndexed { index, source -> - val httpSource = source as HttpSource - if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) { - SourceTable.insert { - it[this.id] = httpSource.id - it[name] = httpSource.name - it[this.lang] = httpSource.lang - it[extension] = extensionId - it[partOfFactorySource] = true - } - } - logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}") - } - } - } - else -> { - throw RuntimeException("Extension content is unexpected") - } } + else -> { + throw RuntimeException("Extension content is unexpected") + } + } - // update extension info - transaction { - ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord.pkgName }) { - it[isInstalled] = true - it[classFQName] = className - } + // update extension info + transaction { + ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord.pkgName }) { + it[isInstalled] = true + it[classFQName] = className } } return 201 // we downloaded successfully @@ -171,16 +169,18 @@ object Extension { } } - val networkHelper: NetworkHelper by injectLazy() + private val network: NetworkHelper by injectLazy() - private fun downloadAPKFile(url: String, apkPath: String) { + private suspend fun downloadAPKFile(url: String, apkPath: String) { val request = Request.Builder().url(url).build() - val response = networkHelper.client.newCall(request).execute() + val response = network.client.newCall(request).await() val downloadedFile = File(apkPath) - val sink = downloadedFile.sink().buffer() - sink.writeAll(response.body!!.source()) - sink.close() + downloadedFile.sink().buffer().use { sink -> + response.body!!.source().use { source -> + sink.writeAll(source) + } + } } fun uninstallExtension(pkgName: String) { @@ -206,7 +206,7 @@ object Extension { } } - fun updateExtension(pkgName: String): Int { + suspend fun updateExtension(pkgName: String): Int { val targetExtension = ExtensionsList.updateMap.remove(pkgName)!! uninstallExtension(pkgName) transaction { @@ -224,9 +224,7 @@ object Extension { return installExtension(pkgName) } - val network: NetworkHelper by injectLazy() - - fun getExtensionIcon(apkName: String): Pair { + suspend fun getExtensionIcon(apkName: String): Pair { val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl] val saveDir = "${ApplicationDirs.extensionsRoot}/icon" @@ -234,7 +232,7 @@ object Extension { return getCachedImageResponse(saveDir, apkName) { network.client.newCall( GET(iconUrl) - ).execute() + ).await() } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/ExtensionsList.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/ExtensionsList.kt index 64fa5ad..f036267 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/ExtensionsList.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/ExtensionsList.kt @@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.extension.model.Extension import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl import ir.armor.tachidesk.model.database.ExtensionTable import ir.armor.tachidesk.model.dataclass.ExtensionDataClass -import kotlinx.coroutines.runBlocking import mu.KotlinLogging import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert @@ -31,15 +30,14 @@ object ExtensionsList { // const val ExtensionUpdateDelayTime = 60 * 1000 // 60,000 milliseconds = 60 seconds const val ExtensionUpdateDelayTime = 60 * 1000 - fun getExtensionList(): List { + suspend fun getExtensionList(): List { // update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) { logger.debug("Getting extensions list from the internet") lastUpdateCheck = System.currentTimeMillis() - runBlocking { - val foundExtensions = ExtensionGithubApi.findExtensions() - updateExtensionDatabase(foundExtensions) - } + + val foundExtensions = ExtensionGithubApi.findExtensions() + updateExtensionDatabase(foundExtensions) } else { logger.debug("used cached extension list") } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Library.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Library.kt index caccf59..ea2c942 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Library.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Library.kt @@ -22,7 +22,7 @@ object Library { // TODO: `Category.isLanding` is to handle the default categories a new library manga gets, // ..implement that shit at some time... // ..also Consider to rename it to `isDefault` - fun addMangaToLibrary(mangaId: Int) { + suspend fun addMangaToLibrary(mangaId: Int) { val manga = getManga(mangaId) if (!manga.inLibrary) { transaction { @@ -33,7 +33,7 @@ object Library { } } - fun removeMangaFromLibrary(mangaId: Int) { + suspend fun removeMangaFromLibrary(mangaId: Int) { val manga = getManga(mangaId) if (manga.inLibrary) { transaction { diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt index f67a846..48be802 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt @@ -9,6 +9,7 @@ package ir.armor.tachidesk.impl import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.util.lang.awaitSingle import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.impl.Source.getSource @@ -16,13 +17,14 @@ import ir.armor.tachidesk.model.database.MangaStatus import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.dataclass.MangaDataClass import ir.armor.tachidesk.server.ApplicationDirs +import ir.armor.tachidesk.util.await import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import java.io.InputStream object Manga { - fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass { + suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass { var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } return if (mangaEntry[MangaTable.initialized]) { @@ -51,7 +53,7 @@ object Manga { url = mangaEntry[MangaTable.url] title = mangaEntry[MangaTable.title] } - ).toBlocking().first() + ).awaitSingle() transaction { MangaTable.update({ MangaTable.id eq mangaId }) { @@ -92,7 +94,7 @@ object Manga { } } - fun getMangaThumbnail(mangaId: Int): Pair { + suspend fun getMangaThumbnail(mangaId: Int): Pair { val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } val saveDir = ApplicationDirs.thumbnailsRoot val fileName = mangaId.toString() @@ -107,7 +109,7 @@ object Manga { source.client.newCall( GET(thumbnailUrl, source.headers) - ).execute() + ).await() } } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt index 5ee9a6f..e68f0d3 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt @@ -8,6 +8,7 @@ package ir.armor.tachidesk.impl * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.util.lang.awaitSingle import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.model.database.MangaStatus import ir.armor.tachidesk.model.database.MangaTable @@ -22,13 +23,13 @@ object MangaList { return "/api/v1/manga/$mangaId/thumbnail" } - fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { - val source = getHttpSource(sourceId.toLong()) + suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { + val source = getHttpSource(sourceId) val mangasPage = if (popular) { - source.fetchPopularManga(pageNum).toBlocking().first() + source.fetchPopularManga(pageNum).awaitSingle() } else { if (source.supportsLatest) - source.fetchLatestUpdates(pageNum).toBlocking().first() + source.fetchLatestUpdates(pageNum).awaitSingle() else throw Exception("Source $source doesn't support latest") } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt index e36125e..c953de0 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt @@ -9,6 +9,7 @@ package ir.armor.tachidesk.impl import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.lang.awaitSingle import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.model.database.ChapterTable import ir.armor.tachidesk.model.database.MangaTable @@ -27,14 +28,14 @@ object Page { * A page might have a imageUrl ready from the get go, or we might need to * go an extra step and call fetchImageUrl to get it. */ - fun getTrueImageUrl(page: Page, source: HttpSource): String { + suspend fun getTrueImageUrl(page: Page, source: HttpSource): String { if (page.imageUrl == null) { - page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!! + page.imageUrl = source.fetchImageUrl(page).awaitSingle() } return page.imageUrl!! } - fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair { + suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair { val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val chapterEntry = transaction { @@ -53,9 +54,10 @@ object Page { ) if (pageEntry[PageTable.imageUrl] == null) { + val trueImageUrl = getTrueImageUrl(tachiPage, source) transaction { PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) { - it[imageUrl] = getTrueImageUrl(tachiPage, source) + it[imageUrl] = trueImageUrl } } } @@ -65,7 +67,7 @@ object Page { val fileName = index.toString() return getCachedImageResponse(saveDir, fileName) { - source.fetchImage(tachiPage).toBlocking().first() + source.fetchImage(tachiPage).awaitSingle() } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Search.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Search.kt index af103f0..37378b8 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Search.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Search.kt @@ -7,6 +7,7 @@ package ir.armor.tachidesk.impl * 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/. */ +import eu.kanade.tachiyomi.util.lang.awaitSingle import ir.armor.tachidesk.impl.MangaList.processEntries import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass @@ -18,9 +19,9 @@ object Search { // source.getFilterList().toItems() } - fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { + suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { val source = getHttpSource(sourceId) - val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first() + val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle() return searchManga.processEntries(sourceId) } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/File.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/File.kt index 442c390..c68141c 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/File.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/File.kt @@ -55,7 +55,7 @@ private fun BufferedSource.saveTo(stream: OutputStream) { } } -fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair { +suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair { val cachedFile = findFileNameStartingWith(saveDir, fileName) val filePath = "$saveDir/$fileName" if (cachedFile != null) { diff --git a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt index 999590c..dbcdee5 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt @@ -31,6 +31,11 @@ import ir.armor.tachidesk.impl.Search.sourceSearch import ir.armor.tachidesk.impl.Source.getSource import ir.armor.tachidesk.impl.Source.getSourceList import ir.armor.tachidesk.server.util.openInBrowser +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.future.future import mu.KotlinLogging import java.io.IOException @@ -43,6 +48,7 @@ import java.io.IOException object JavalinSetup { private val logger = KotlinLogging.logger {} + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) fun javalinSetup() { var hasWebUiBundled = false @@ -75,22 +81,24 @@ object JavalinSetup { } app.get("/api/v1/extension/list") { ctx -> - ctx.json(getExtensionList()) + ctx.json(scope.future { getExtensionList() }) } app.get("/api/v1/extension/install/:pkgName") { ctx -> val pkgName = ctx.pathParam("pkgName") + // TODO maybe replace with ctx.result(scope.future { installExtension(pkgName) })? ctx.status( - installExtension(pkgName) + scope.future { installExtension(pkgName) }.get() ) } app.get("/api/v1/extension/update/:pkgName") { ctx -> val pkgName = ctx.pathParam("pkgName") + // TODO maybe replace with ctx.result(scope.future { updateExtension(pkgName) })? ctx.status( - updateExtension(pkgName) + scope.future { updateExtension(pkgName) }.get() ) } @@ -104,7 +112,8 @@ object JavalinSetup { // icon for extension named `apkName` app.get("/api/v1/extension/icon/:apkName") { ctx -> val apkName = ctx.pathParam("apkName") - val result = getExtensionIcon(apkName) + // TODO see if there is a better way + val result = scope.future { getExtensionIcon(apkName) }.get() ctx.result(result.first) ctx.header("content-type", result.second) @@ -125,26 +134,27 @@ object JavalinSetup { app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> val sourceId = ctx.pathParam("sourceId").toLong() val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json(getMangaList(sourceId, pageNum, popular = true)) + ctx.json(scope.future { getMangaList(sourceId, pageNum, popular = true) }) } // latest mangas from source with id `sourceId` app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx -> val sourceId = ctx.pathParam("sourceId").toLong() val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json(getMangaList(sourceId, pageNum, popular = false)) + ctx.json(scope.future { getMangaList(sourceId, pageNum, popular = false) }) } // get manga info app.get("/api/v1/manga/:mangaId/") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() - ctx.json(getManga(mangaId)) + ctx.json(scope.future { getManga(mangaId) }) } // manga thumbnail app.get("api/v1/manga/:mangaId/thumbnail") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() - val result = getMangaThumbnail(mangaId) + // TODO see if there is a better way + val result = scope.future { getMangaThumbnail(mangaId) }.get() ctx.result(result.first) ctx.header("content-type", result.second) @@ -153,14 +163,16 @@ object JavalinSetup { // adds the manga to library app.get("api/v1/manga/:mangaId/library") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() - addMangaToLibrary(mangaId) + // TODO see if there is a better way + scope.future { addMangaToLibrary(mangaId) }.get() ctx.status(200) } // removes the manga from the library app.delete("api/v1/manga/:mangaId/library") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() - removeMangaFromLibrary(mangaId) + // TODO see if there is a better way + scope.future { removeMangaFromLibrary(mangaId) }.get() ctx.status(200) } @@ -188,20 +200,21 @@ object JavalinSetup { app.get("/api/v1/manga/:mangaId/chapters") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() - ctx.json(getChapterList(mangaId)) + ctx.json(scope.future { getChapterList(mangaId) }) } app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx -> val chapterIndex = ctx.pathParam("chapterIndex").toInt() val mangaId = ctx.pathParam("mangaId").toInt() - ctx.json(getChapter(chapterIndex, mangaId)) + ctx.json(scope.future { getChapter(chapterIndex, mangaId) }) } app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt() val index = ctx.pathParam("index").toInt() - val result = getPageImage(mangaId, chapterIndex, index) + // TODO see if there is a better way + val result = scope.future { getPageImage(mangaId, chapterIndex, index) }.get() ctx.result(result.first) ctx.header("content-type", result.second) @@ -218,7 +231,7 @@ object JavalinSetup { val sourceId = ctx.pathParam("sourceId").toLong() val searchTerm = ctx.pathParam("searchTerm") val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json(sourceSearch(sourceId, searchTerm, pageNum)) + ctx.json(scope.future { sourceSearch(sourceId, searchTerm, pageNum) }) } // source filter list diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/OkHttp.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/OkHttp.kt new file mode 100644 index 0000000..ed7275b --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/OkHttp.kt @@ -0,0 +1,41 @@ +package ir.armor.tachidesk.util + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +// Based on https://github.com/gildor/kotlin-coroutines-okhttp +suspend fun Call.await(): Response { + return suspendCancellableCoroutine { continuation -> + enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + continuation.resumeWithException(Exception("HTTP error ${response.code}")) + return + } + + continuation.resume(response) + } + + override fun onFailure(call: Call, e: IOException) { + // Don't bother with resuming the continuation if it is already cancelled. + if (continuation.isCancelled) return + continuation.resumeWithException(e) + } + } + ) + + continuation.invokeOnCancellation { + try { + cancel() + } catch (ex: Throwable) { + // Ignore cancel exception + } + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/RxCoroutineBridge.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/RxCoroutineBridge.kt new file mode 100644 index 0000000..caa06a8 --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/RxCoroutineBridge.kt @@ -0,0 +1,60 @@ +package eu.kanade.tachiyomi.util.lang + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import rx.Emitter +import rx.Observable +import rx.Subscriber +import rx.Subscription +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/* + * Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY. + */ + +suspend fun Observable.awaitSingle(): T = single().awaitOne() + +private suspend fun Observable.awaitOne(): T = suspendCancellableCoroutine { cont -> + cont.unsubscribeOnCancellation( + subscribe( + object : Subscriber() { + override fun onStart() { + request(1) + } + + override fun onNext(t: T) { + cont.resume(t) + } + + override fun onCompleted() { + if (cont.isActive) cont.resumeWithException( + IllegalStateException( + "Should have invoked onNext" + ) + ) + } + + override fun onError(e: Throwable) { + /* + * Rx1 observable throws NoSuchElementException if cancellation happened before + * element emission. To mitigate this we try to atomically resume continuation with exception: + * if resume failed, then we know that continuation successfully cancelled itself + */ + val token = cont.tryResumeWithException(e) + if (token != null) { + cont.completeResume(token) + } + } + } + ) + ) +} + +internal fun CancellableContinuation.unsubscribeOnCancellation(sub: Subscription) = + invokeOnCancellation { sub.unsubscribe() }