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 cba128e..3a02566 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt @@ -9,24 +9,22 @@ 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.impl.util.awaitSingle 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 import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update object Chapter { + /** get chapter list when showing a manga */ suspend fun getChapterList(mangaId: Int): List { val mangaDetails = getManga(mangaId) val source = getHttpSource(mangaDetails.sourceId.toLong()) @@ -44,7 +42,7 @@ object Chapter { chapterList.reversed().forEachIndexed { index, fetchedChapter -> val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() if (chapterEntry == null) { - ChapterTable.insertAndGetId { + ChapterTable.insert { it[url] = fetchedChapter.url it[name] = fetchedChapter.name it[date_upload] = fetchedChapter.date_upload @@ -67,89 +65,75 @@ object Chapter { } } - // clear any orphaned chapters + // clear any orphaned chapters that are in the db but not in `chapterList` val dbChapterCount = transaction { ChapterTable.selectAll().count() } if (dbChapterCount > chapterCount) { // we got some clean up due // TODO: delete orphan chapters } - chapterList.mapIndexed { index, it -> + chapterList.map { it -> ChapterDataClass( - ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value, it.url, it.name, it.date_upload, it.chapter_number, it.scanlator, mangaId, - chapterCount - index, - chapterCount ) } } } + /** used to display a chapter, get a chapter in order to show it's pages */ 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) + val chapterEntry = transaction { + ChapterTable.select { + (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) }.firstOrNull()!! - val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! - source = getHttpSource(mangaEntry[MangaTable.sourceReference]) - sChapter = SChapter.create().apply { - url = chapterEntry!![ChapterTable.url] - name = chapterEntry!![ChapterTable.name] - } } - val pageList = source!!.fetchPageList( - sChapter!! + val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) + + val pageList = source.fetchPageList( + SChapter.create().apply { + url = chapterEntry[ChapterTable.url] + name = chapterEntry[ChapterTable.name] + } ).awaitSingle() - return transaction { - val chapterRow = chapterEntry!! - - val chapterId = chapterRow[ChapterTable.id].value - val chapterCount = transaction { ChapterTable.selectAll().count() } - - val chapter = ChapterDataClass( - chapterId, - chapterRow[ChapterTable.url], - chapterRow[ChapterTable.name], - chapterRow[ChapterTable.date_upload], - chapterRow[ChapterTable.chapter_number], - chapterRow[ChapterTable.scanlator], - mangaId, - chapterRow[ChapterTable.chapterIndex], - chapterCount.toInt(), - - pageList.count() - ) + val chapterId = chapterEntry[ChapterTable.id].value + val chapterCount = transaction { ChapterTable.selectAll().count() } + // update page list for this chapter + transaction { pageList.forEach { page -> val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() } if (pageEntry == null) { - transaction { - PageTable.insert { - it[index] = page.index - it[url] = page.url - it[imageUrl] = page.imageUrl - it[this.chapter] = chapterId - } + PageTable.insert { + it[index] = page.index + it[url] = page.url + it[imageUrl] = page.imageUrl + it[chapter] = chapterId } } else { - transaction { - PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) { - it[url] = page.url - it[imageUrl] = page.imageUrl - } + PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) { + it[url] = page.url + it[imageUrl] = page.imageUrl } } } - - chapter } + + return ChapterDataClass( + chapterEntry[ChapterTable.url], + chapterEntry[ChapterTable.name], + chapterEntry[ChapterTable.date_upload], + chapterEntry[ChapterTable.chapter_number], + chapterEntry[ChapterTable.scanlator], + mangaId, + chapterEntry[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 bb0147c..3e3c9e9 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt @@ -17,10 +17,11 @@ import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.online.HttpSource import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass import ir.armor.tachidesk.impl.util.APKExtractor +import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse +import ir.armor.tachidesk.impl.util.await import ir.armor.tachidesk.model.database.ExtensionTable import ir.armor.tachidesk.model.database.SourceTable import ir.armor.tachidesk.server.ApplicationDirs -import ir.armor.tachidesk.util.await import mu.KotlinLogging import okhttp3.Request import okio.buffer @@ -136,7 +137,7 @@ object Extension { } is SourceFactory -> { // theme source or multi lang transaction { - instance.createSources().forEachIndexed { index, source -> + instance.createSources().forEach { source -> val httpSource = source as HttpSource if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) { SourceTable.insert { 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 48be802..f759e1b 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt @@ -9,15 +9,16 @@ 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 +import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse +import ir.armor.tachidesk.impl.util.await +import ir.armor.tachidesk.impl.util.awaitSingle 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 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 e68f0d3..8a78ff7 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt @@ -8,8 +8,8 @@ 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.impl.util.awaitSingle import ir.armor.tachidesk.model.database.MangaStatus import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.dataclass.MangaDataClass 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 c953de0..85314c9 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt @@ -9,8 +9,9 @@ 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.impl.util.CachedImageResponse.getCachedImageResponse +import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.model.database.ChapterTable import ir.armor.tachidesk.model.database.MangaTable import ir.armor.tachidesk.model.database.PageTable 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 37378b8..bd0f968 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Search.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Search.kt @@ -7,9 +7,9 @@ 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.impl.util.awaitSingle import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass object Search { diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt index 17a6649..ec65c32 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt @@ -175,8 +175,8 @@ object APKExtractor { return compXmlStringAt(xml, strOff) } - var spaces = " " - fun prtIndent(indent: Int, str: String) { + private var spaces = " " + private fun prtIndent(indent: Int, str: String) { logger.debug(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str) } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt new file mode 100644 index 0000000..9eaf5ad --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt @@ -0,0 +1,67 @@ +package ir.armor.tachidesk.impl.util + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * 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/. */ + +import okhttp3.Response +import okio.buffer +import okio.sink +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Paths + +object CachedImageResponse { + private fun pathToInputStream(path: String): InputStream { + return BufferedInputStream(FileInputStream(path)) + } + + private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? { + File(directoryPath).listFiles().forEach { file -> + if (file.name.startsWith(fileName)) + return "$directoryPath/${file.name}" + } + return null + } + + /** fetch a cached image response, calls `fetcher` if cache fails */ + suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair { + val cachedFile = findFileNameStartingWith(saveDir, fileName) + val filePath = "$saveDir/$fileName" + if (cachedFile != null) { + val fileType = cachedFile.substringAfter(filePath) + return Pair( + pathToInputStream(cachedFile), + "image/$fileType" + ) + } + + val response = fetcher() + + if (response.code == 200) { + val contentType = response.headers["content-type"]!! + val fullPath = filePath + "." + contentType.substringAfter("image/") + + Files.newOutputStream(Paths.get(fullPath)).use { output -> + response.body!!.source().use { input -> + output.sink().buffer().use { + it.writeAll(input) + it.flush() + } + } + } + return Pair( + pathToInputStream(fullPath), + contentType + ) + } else { + throw Exception("request error! ${response.code}") + } + } +} 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 deleted file mode 100644 index c68141c..0000000 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/File.kt +++ /dev/null @@ -1,85 +0,0 @@ -package ir.armor.tachidesk.impl - -/* - * Copyright (C) Contributors to the Suwayomi project - * - * 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/. */ - -import okhttp3.Response -import okio.BufferedSource -import okio.buffer -import okio.sink -import java.io.BufferedInputStream -import java.io.File -import java.io.FileInputStream -import java.io.InputStream -import java.io.OutputStream -import java.nio.file.Files -import java.nio.file.Paths - -// fun writeStream(fileStream: InputStream, path: String) { -// Files.newOutputStream(Paths.get(path)).use { os -> -// val buffer = ByteArray(128 * 1024) -// var len: Int -// while (fileStream.read(buffer).also { len = it } > 0) { -// os.write(buffer, 0, len) -// } -// } -// } - -fun pathToInputStream(path: String): InputStream { - return BufferedInputStream(FileInputStream(path)) -} - -fun findFileNameStartingWith(directoryPath: String, fileName: String): String? { - File(directoryPath).listFiles().forEach { file -> - if (file.name.startsWith(fileName)) - return "$directoryPath/${file.name}" - } - return null -} - -/** - * Saves the given source to an output stream and closes both resources. - * - * @param stream the stream where the source is copied. - */ -private fun BufferedSource.saveTo(stream: OutputStream) { - use { input -> - stream.sink().buffer().use { - it.writeAll(input) - it.flush() - } - } -} - -suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair { - val cachedFile = findFileNameStartingWith(saveDir, fileName) - val filePath = "$saveDir/$fileName" - if (cachedFile != null) { - val fileType = cachedFile.substringAfter(filePath) - return Pair( - pathToInputStream(cachedFile), - "image/$fileType" - ) - } - - val response = fetcher() - - if (response.code == 200) { - val contentType = response.headers["content-type"]!! - val fullPath = filePath + "." + contentType.substringAfter("image/") - - Files.newOutputStream(Paths.get(fullPath)).use { os -> - response.body!!.source().saveTo(os) - } - return Pair( - pathToInputStream(fullPath), - contentType - ) - } else { - throw Exception("request error! ${response.code}") - } -} diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/OkHttp.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/OkHttp.kt similarity index 80% rename from server/src/main/kotlin/ir/armor/tachidesk/util/OkHttp.kt rename to server/src/main/kotlin/ir/armor/tachidesk/impl/util/OkHttp.kt index ed7275b..825cd06 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/util/OkHttp.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/OkHttp.kt @@ -1,4 +1,11 @@ -package ir.armor.tachidesk.util +package ir.armor.tachidesk.impl.util + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * 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/. */ import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.Call @@ -38,4 +45,4 @@ suspend fun Call.await(): Response { } } } -} \ 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/impl/util/RxCoroutineBridge.kt similarity index 75% rename from server/src/main/kotlin/ir/armor/tachidesk/util/RxCoroutineBridge.kt rename to server/src/main/kotlin/ir/armor/tachidesk/impl/util/RxCoroutineBridge.kt index caa06a8..d41fb1a 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/util/RxCoroutineBridge.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/RxCoroutineBridge.kt @@ -1,19 +1,21 @@ -package eu.kanade.tachiyomi.util.lang +package ir.armor.tachidesk.impl.util + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * 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/. */ 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 +// source: https://github.com/jobobby04/TachiyomiSY/blob/9320221a4e8b118ef68deb60d8c4c32bcbb9e06f/app/src/main/java/eu/kanade/tachiyomi/util/lang/RxCoroutineBridge.kt /* * Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY. */ @@ -56,5 +58,5 @@ private suspend fun Observable.awaitOne(): T = suspendCancellableCoroutin ) } -internal fun CancellableContinuation.unsubscribeOnCancellation(sub: Subscription) = +private fun CancellableContinuation.unsubscribeOnCancellation(sub: Subscription) = invokeOnCancellation { sub.unsubscribe() } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/ChapterDataClass.kt b/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/ChapterDataClass.kt index 0249a0d..9260692 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/ChapterDataClass.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/ChapterDataClass.kt @@ -8,14 +8,19 @@ package ir.armor.tachidesk.model.dataclass * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ data class ChapterDataClass( - val id: Int, val url: String, val name: String, val date_upload: Long, val chapter_number: Float, val scanlator: String?, val mangaId: Int, - val chapterIndex: Int, - val chapterCount: Int, + + /** this chapter's index */ + val chapterIndex: Int? = null, + + /** total chapter count, used to calculate if there's a next and prev chapter */ + val chapterCount: Int? = null, + + /** used to construct pages in the front-end */ val pageCount: Int? = 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 dbcdee5..66526ca 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt @@ -33,7 +33,6 @@ 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 @@ -198,11 +197,13 @@ object JavalinSetup { ctx.status(200) } + // get chapter list when showing a manga app.get("/api/v1/manga/:mangaId/chapters") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() ctx.json(scope.future { getChapterList(mangaId) }) } + // used to display a chapter, get a chapter in order to show it's pages app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx -> val chapterIndex = ctx.pathParam("chapterIndex").toInt() val mangaId = ctx.pathParam("mangaId").toInt()