Merge pull request #46 from Syer10/future

Implement coroutines
This commit is contained in:
Aria Moradi 2021-04-02 02:53:59 +04:30 committed by GitHub
commit f983f0e359
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 260 additions and 124 deletions

View File

@ -61,6 +61,7 @@ dependencies {
val coroutinesVersion = "1.3.9" val coroutinesVersion = "1.3.9"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") 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 // dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon") implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
@ -135,6 +136,15 @@ tasks {
archiveVersion.set(TachideskVersion) archiveVersion.set(TachideskVersion)
archiveClassifier.set(TachideskRevision) archiveClassifier.set(TachideskRevision)
} }
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
)
}
}
} }
launch4j { //used for windows launch4j { //used for windows
@ -195,7 +205,7 @@ tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
} }
tasks.withType<ShadowJar> { tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build") destinationDirectory.set(File("$rootDir/server/build"))
dependsOn("formatKotlin", "lintKotlin") dependsOn("formatKotlin", "lintKotlin")
} }

View File

@ -9,12 +9,15 @@ package ir.armor.tachidesk.impl
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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.Manga.getManga
import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.impl.Source.getHttpSource
import ir.armor.tachidesk.model.database.ChapterTable import ir.armor.tachidesk.model.database.ChapterTable
import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.database.MangaTable
import ir.armor.tachidesk.model.database.PageTable import ir.armor.tachidesk.model.database.PageTable
import ir.armor.tachidesk.model.dataclass.ChapterDataClass import ir.armor.tachidesk.model.dataclass.ChapterDataClass
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
@ -24,7 +27,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
object Chapter { object Chapter {
fun getChapterList(mangaId: Int): List<ChapterDataClass> { suspend fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId.toLong()) val source = getHttpSource(mangaDetails.sourceId.toLong())
@ -33,7 +36,7 @@ object Chapter {
title = mangaDetails.title title = mangaDetails.title
url = mangaDetails.url url = mangaDetails.url
} }
).toBlocking().first() ).awaitSingle()
val chapterCount = chapterList.count() val chapterCount = chapterList.count()
@ -86,33 +89,40 @@ object Chapter {
} }
} }
fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass { suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
return transaction { var chapterEntry: ResultRow? = null
val chapterEntry = ChapterTable.select { var source: HttpSource? = null
var sChapter: SChapter? = null
transaction {
chapterEntry = ChapterTable.select {
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId) ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
}.firstOrNull()!! }.firstOrNull()!!
val mangaEntry = MangaTable.select { MangaTable.id 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( return transaction {
SChapter.create().apply { val chapterRow = chapterEntry!!
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).toBlocking().first()
val chapterId = chapterEntry[ChapterTable.id].value val chapterId = chapterRow[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() } val chapterCount = transaction { ChapterTable.selectAll().count() }
val chapter = ChapterDataClass( val chapter = ChapterDataClass(
chapterId, chapterId,
chapterEntry[ChapterTable.url], chapterRow[ChapterTable.url],
chapterEntry[ChapterTable.name], chapterRow[ChapterTable.name],
chapterEntry[ChapterTable.date_upload], chapterRow[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number], chapterRow[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator], chapterRow[ChapterTable.scanlator],
mangaId, mangaId,
chapterEntry[ChapterTable.chapterIndex], chapterRow[ChapterTable.chapterIndex],
chapterCount.toInt(), chapterCount.toInt(),
pageList.count() pageList.count()

View File

@ -20,7 +20,7 @@ import ir.armor.tachidesk.impl.util.APKExtractor
import ir.armor.tachidesk.model.database.ExtensionTable import ir.armor.tachidesk.model.database.ExtensionTable
import ir.armor.tachidesk.model.database.SourceTable import ir.armor.tachidesk.model.database.SourceTable
import ir.armor.tachidesk.server.ApplicationDirs import ir.armor.tachidesk.server.ApplicationDirs
import kotlinx.coroutines.runBlocking import ir.armor.tachidesk.util.await
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
@ -86,7 +86,7 @@ object Extension {
return classToLoad.getDeclaredConstructor().newInstance() return classToLoad.getDeclaredConstructor().newInstance()
} }
fun installExtension(pkgName: String): Int { suspend fun installExtension(pkgName: String): Int {
logger.debug("Installing $pkgName") logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName } val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
val fileNameWithoutType = extensionRecord.apkName.substringBefore(".apk") val fileNameWithoutType = extensionRecord.apkName.substringBefore(".apk")
@ -95,74 +95,72 @@ object Extension {
// check if we don't have the dex file already downloaded // check if we don't have the dex file already downloaded
val jarPath = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType.jar" val jarPath = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(jarPath).exists()) { if (!File(jarPath).exists()) {
runBlocking { val apkToDownload = ExtensionGithubApi.getApkUrl(extensionRecord)
val apkToDownload = ExtensionGithubApi.getApkUrl(extensionRecord)
val apkFilePath = "$dirPathWithoutType.apk" val apkFilePath = "$dirPathWithoutType.apk"
val jarFilePath = "$dirPathWithoutType.jar" val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex" val dexFilePath = "$dirPathWithoutType.dex"
// download apk file // download apk file
downloadAPKFile(apkToDownload, apkFilePath) downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath) val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath)
logger.debug(className) logger.debug(className)
// dex -> jar // dex -> jar
dex2jar(dexFilePath, jarFilePath, fileNameWithoutType) dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
// clean up // clean up
File(apkFilePath).delete() File(apkFilePath).delete()
File(dexFilePath).delete() File(dexFilePath).delete()
// update sources of the extension // update sources of the extension
val instance = loadExtensionInstance(jarFilePath, className) val instance = loadExtensionInstance(jarFilePath, className)
val extensionId = transaction { val extensionId = transaction {
ExtensionTable.select { ExtensionTable.pkgName eq extensionRecord.pkgName }.firstOrNull()!![ExtensionTable.id] 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}")
}
} }
is SourceFactory -> { // theme source or multi lang
when (instance) { transaction {
is HttpSource -> { // single source instance.createSources().forEachIndexed { index, source ->
transaction { val httpSource = source as HttpSource
if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) { if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert { SourceTable.insert {
it[this.id] = instance.id it[this.id] = httpSource.id
it[name] = instance.name it[name] = httpSource.name
it[this.lang] = instance.lang it[this.lang] = httpSource.lang
it[extension] = extensionId 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 // update extension info
transaction { transaction {
ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord.pkgName }) { ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord.pkgName }) {
it[isInstalled] = true it[isInstalled] = true
it[classFQName] = className it[classFQName] = className
}
} }
} }
return 201 // we downloaded successfully 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 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 downloadedFile = File(apkPath)
val sink = downloadedFile.sink().buffer() downloadedFile.sink().buffer().use { sink ->
sink.writeAll(response.body!!.source()) response.body!!.source().use { source ->
sink.close() sink.writeAll(source)
}
}
} }
fun uninstallExtension(pkgName: String) { 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)!! val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
uninstallExtension(pkgName) uninstallExtension(pkgName)
transaction { transaction {
@ -224,9 +224,7 @@ object Extension {
return installExtension(pkgName) return installExtension(pkgName)
} }
val network: NetworkHelper by injectLazy() suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl] val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
val saveDir = "${ApplicationDirs.extensionsRoot}/icon" val saveDir = "${ApplicationDirs.extensionsRoot}/icon"
@ -234,7 +232,7 @@ object Extension {
return getCachedImageResponse(saveDir, apkName) { return getCachedImageResponse(saveDir, apkName) {
network.client.newCall( network.client.newCall(
GET(iconUrl) GET(iconUrl)
).execute() ).await()
} }
} }

View File

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
import ir.armor.tachidesk.model.database.ExtensionTable import ir.armor.tachidesk.model.database.ExtensionTable
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert 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 // 60,000 milliseconds = 60 seconds
const val ExtensionUpdateDelayTime = 60 * 1000 const val ExtensionUpdateDelayTime = 60 * 1000
fun getExtensionList(): List<ExtensionDataClass> { suspend fun getExtensionList(): List<ExtensionDataClass> {
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty // update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) { if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
logger.debug("Getting extensions list from the internet") logger.debug("Getting extensions list from the internet")
lastUpdateCheck = System.currentTimeMillis() lastUpdateCheck = System.currentTimeMillis()
runBlocking {
val foundExtensions = ExtensionGithubApi.findExtensions() val foundExtensions = ExtensionGithubApi.findExtensions()
updateExtensionDatabase(foundExtensions) updateExtensionDatabase(foundExtensions)
}
} else { } else {
logger.debug("used cached extension list") logger.debug("used cached extension list")
} }

View File

@ -22,7 +22,7 @@ object Library {
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets, // TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
// ..implement that shit at some time... // ..implement that shit at some time...
// ..also Consider to rename it to `isDefault` // ..also Consider to rename it to `isDefault`
fun addMangaToLibrary(mangaId: Int) { suspend fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId) val manga = getManga(mangaId)
if (!manga.inLibrary) { if (!manga.inLibrary) {
transaction { transaction {
@ -33,7 +33,7 @@ object Library {
} }
} }
fun removeMangaFromLibrary(mangaId: Int) { suspend fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId) val manga = getManga(mangaId)
if (manga.inLibrary) { if (manga.inLibrary) {
transaction { transaction {

View File

@ -9,6 +9,7 @@ package ir.armor.tachidesk.impl
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga 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.MangaList.proxyThumbnailUrl
import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.impl.Source.getHttpSource
import ir.armor.tachidesk.impl.Source.getSource 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.database.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass import ir.armor.tachidesk.model.dataclass.MangaDataClass
import ir.armor.tachidesk.server.ApplicationDirs import ir.armor.tachidesk.server.ApplicationDirs
import ir.armor.tachidesk.util.await
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import java.io.InputStream import java.io.InputStream
object Manga { 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()!! } var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
return if (mangaEntry[MangaTable.initialized]) { return if (mangaEntry[MangaTable.initialized]) {
@ -51,7 +53,7 @@ object Manga {
url = mangaEntry[MangaTable.url] url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title] title = mangaEntry[MangaTable.title]
} }
).toBlocking().first() ).awaitSingle()
transaction { transaction {
MangaTable.update({ MangaTable.id eq mangaId }) { MangaTable.update({ MangaTable.id eq mangaId }) {
@ -92,7 +94,7 @@ object Manga {
} }
} }
fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> { suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = ApplicationDirs.thumbnailsRoot val saveDir = ApplicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
@ -107,7 +109,7 @@ object Manga {
source.client.newCall( source.client.newCall(
GET(thumbnailUrl, source.headers) GET(thumbnailUrl, source.headers)
).execute() ).await()
} }
} }
} }

View File

@ -8,6 +8,7 @@ package ir.armor.tachidesk.impl
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.MangasPage 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.impl.Source.getHttpSource
import ir.armor.tachidesk.model.database.MangaStatus import ir.armor.tachidesk.model.database.MangaStatus
import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.database.MangaTable
@ -22,13 +23,13 @@ object MangaList {
return "/api/v1/manga/$mangaId/thumbnail" return "/api/v1/manga/$mangaId/thumbnail"
} }
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong()) val source = getHttpSource(sourceId)
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first() source.fetchPopularManga(pageNum).awaitSingle()
} else { } else {
if (source.supportsLatest) if (source.supportsLatest)
source.fetchLatestUpdates(pageNum).toBlocking().first() source.fetchLatestUpdates(pageNum).awaitSingle()
else else
throw Exception("Source $source doesn't support latest") throw Exception("Source $source doesn't support latest")
} }

View File

@ -9,6 +9,7 @@ package ir.armor.tachidesk.impl
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource 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.impl.Source.getHttpSource
import ir.armor.tachidesk.model.database.ChapterTable import ir.armor.tachidesk.model.database.ChapterTable
import ir.armor.tachidesk.model.database.MangaTable 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 * 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. * 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) { if (page.imageUrl == null) {
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!! page.imageUrl = source.fetchImageUrl(page).awaitSingle()
} }
return page.imageUrl!! return page.imageUrl!!
} }
fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> { suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { val chapterEntry = transaction {
@ -53,9 +54,10 @@ object Page {
) )
if (pageEntry[PageTable.imageUrl] == null) { if (pageEntry[PageTable.imageUrl] == null) {
val trueImageUrl = getTrueImageUrl(tachiPage, source)
transaction { transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) { 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() val fileName = index.toString()
return getCachedImageResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first() source.fetchImage(tachiPage).awaitSingle()
} }
} }

View File

@ -7,6 +7,7 @@ package ir.armor.tachidesk.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * 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.MangaList.processEntries
import ir.armor.tachidesk.impl.Source.getHttpSource import ir.armor.tachidesk.impl.Source.getHttpSource
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
@ -18,9 +19,9 @@ object Search {
// source.getFilterList().toItems() // 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 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) return searchManga.processEntries(sourceId)
} }

View File

@ -55,7 +55,7 @@ private fun BufferedSource.saveTo(stream: OutputStream) {
} }
} }
fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> { suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair<InputStream, String> {
val cachedFile = findFileNameStartingWith(saveDir, fileName) val cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName" val filePath = "$saveDir/$fileName"
if (cachedFile != null) { if (cachedFile != null) {

View File

@ -31,6 +31,11 @@ import ir.armor.tachidesk.impl.Search.sourceSearch
import ir.armor.tachidesk.impl.Source.getSource import ir.armor.tachidesk.impl.Source.getSource
import ir.armor.tachidesk.impl.Source.getSourceList import ir.armor.tachidesk.impl.Source.getSourceList
import ir.armor.tachidesk.server.util.openInBrowser 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 mu.KotlinLogging
import java.io.IOException import java.io.IOException
@ -43,6 +48,7 @@ import java.io.IOException
object JavalinSetup { object JavalinSetup {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun javalinSetup() { fun javalinSetup() {
var hasWebUiBundled = false var hasWebUiBundled = false
@ -75,22 +81,24 @@ object JavalinSetup {
} }
app.get("/api/v1/extension/list") { ctx -> app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList()) ctx.json(scope.future { getExtensionList() })
} }
app.get("/api/v1/extension/install/:pkgName") { ctx -> app.get("/api/v1/extension/install/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
// TODO maybe replace with ctx.result(scope.future { installExtension(pkgName) })?
ctx.status( ctx.status(
installExtension(pkgName) scope.future { installExtension(pkgName) }.get()
) )
} }
app.get("/api/v1/extension/update/:pkgName") { ctx -> app.get("/api/v1/extension/update/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName") val pkgName = ctx.pathParam("pkgName")
// TODO maybe replace with ctx.result(scope.future { updateExtension(pkgName) })?
ctx.status( ctx.status(
updateExtension(pkgName) scope.future { updateExtension(pkgName) }.get()
) )
} }
@ -104,7 +112,8 @@ object JavalinSetup {
// icon for extension named `apkName` // icon for extension named `apkName`
app.get("/api/v1/extension/icon/:apkName") { ctx -> app.get("/api/v1/extension/icon/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName") 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.result(result.first)
ctx.header("content-type", result.second) ctx.header("content-type", result.second)
@ -125,26 +134,27 @@ object JavalinSetup {
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() 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` // latest mangas from source with id `sourceId`
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() 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 // get manga info
app.get("/api/v1/manga/:mangaId/") { ctx -> app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getManga(mangaId)) ctx.json(scope.future { getManga(mangaId) })
} }
// manga thumbnail // manga thumbnail
app.get("api/v1/manga/:mangaId/thumbnail") { ctx -> app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() 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.result(result.first)
ctx.header("content-type", result.second) ctx.header("content-type", result.second)
@ -153,14 +163,16 @@ object JavalinSetup {
// adds the manga to library // adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx -> app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
addMangaToLibrary(mangaId) // TODO see if there is a better way
scope.future { addMangaToLibrary(mangaId) }.get()
ctx.status(200) ctx.status(200)
} }
// removes the manga from the library // removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx -> app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
removeMangaFromLibrary(mangaId) // TODO see if there is a better way
scope.future { removeMangaFromLibrary(mangaId) }.get()
ctx.status(200) ctx.status(200)
} }
@ -188,20 +200,21 @@ object JavalinSetup {
app.get("/api/v1/manga/:mangaId/chapters") { ctx -> app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() 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 -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").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 -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val chapterIndex = ctx.pathParam("chapterIndex").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val index = ctx.pathParam("index").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.result(result.first)
ctx.header("content-type", result.second) ctx.header("content-type", result.second)
@ -218,7 +231,7 @@ object JavalinSetup {
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum)) ctx.json(scope.future { sourceSearch(sourceId, searchTerm, pageNum) })
} }
// source filter list // source filter list

View File

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

View File

@ -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 <T> Observable<T>.awaitSingle(): T = single().awaitOne()
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
cont.unsubscribeOnCancellation(
subscribe(
object : Subscriber<T>() {
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 <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
invokeOnCancellation { sub.unsubscribe() }