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 1a800f3..0a5789d 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt @@ -27,96 +27,105 @@ import org.jetbrains.exposed.sql.update object Chapter { /** get chapter list when showing a manga */ - suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean): List { - return if (!onlineFetch) { + suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List { + return if (onlineFetch == true) { + getSourceChapters(mangaId) + } else { transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC) .map { ChapterTable.toDataClass(it) } + }.ifEmpty { + // If it was explicitly set to offline dont grab chapters + if (onlineFetch == null) { + getSourceChapters(mangaId) + } else emptyList() } - } else { + } + } - val mangaDetails = getManga(mangaId) - val source = getHttpSource(mangaDetails.sourceId.toLong()) - val chapterList = source.fetchChapterList( - SManga.create().apply { - title = mangaDetails.title - url = mangaDetails.url - } - ).awaitSingle() + private suspend fun getSourceChapters(mangaId: Int): List { + val mangaDetails = getManga(mangaId) + val source = getHttpSource(mangaDetails.sourceId.toLong()) + val chapterList = source.fetchChapterList( + SManga.create().apply { + title = mangaDetails.title + url = mangaDetails.url + } + ).awaitSingle() - val chapterCount = chapterList.count() + val chapterCount = chapterList.count() - transaction { - chapterList.reversed().forEachIndexed { index, fetchedChapter -> - val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() - if (chapterEntry == null) { - ChapterTable.insert { - it[url] = fetchedChapter.url - it[name] = fetchedChapter.name - it[date_upload] = fetchedChapter.date_upload - it[chapter_number] = fetchedChapter.chapter_number - it[scanlator] = fetchedChapter.scanlator + transaction { + chapterList.reversed().forEachIndexed { index, fetchedChapter -> + val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() + if (chapterEntry == null) { + ChapterTable.insert { + it[url] = fetchedChapter.url + it[name] = fetchedChapter.name + it[date_upload] = fetchedChapter.date_upload + it[chapter_number] = fetchedChapter.chapter_number + it[scanlator] = fetchedChapter.scanlator - it[chapterIndex] = index + 1 - it[manga] = mangaId - } - } else { - ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { - it[name] = fetchedChapter.name - it[date_upload] = fetchedChapter.date_upload - it[chapter_number] = fetchedChapter.chapter_number - it[scanlator] = fetchedChapter.scanlator + it[chapterIndex] = index + 1 + it[manga] = mangaId + } + } else { + ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { + it[name] = fetchedChapter.name + it[date_upload] = fetchedChapter.date_upload + it[chapter_number] = fetchedChapter.chapter_number + it[scanlator] = fetchedChapter.scanlator - it[chapterIndex] = index + 1 - it[manga] = mangaId - } + it[chapterIndex] = index + 1 + it[manga] = mangaId } } } + } - // clear any orphaned chapters that are in the db but not in `chapterList` - val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() } - if (dbChapterCount > chapterCount) { // we got some clean up due - val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } } + // clear any orphaned chapters that are in the db but not in `chapterList` + val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() } + if (dbChapterCount > chapterCount) { // we got some clean up due + val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } } - dbChapterList.forEach { - if (it[ChapterTable.chapterIndex] >= chapterList.size || - chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url] - ) { - transaction { - PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] } - ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] } - } + dbChapterList.forEach { + if (it[ChapterTable.chapterIndex] >= chapterList.size || + chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url] + ) { + transaction { + PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] } + ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] } } } } + } - val dbChapterMap = transaction { - ChapterTable.select { ChapterTable.manga eq mangaId } - .associateBy({ it[ChapterTable.url] }, { it }) - } + val dbChapterMap = transaction { + ChapterTable.select { ChapterTable.manga eq mangaId } + .associateBy({ it[ChapterTable.url] }, { it }) + } - return chapterList.mapIndexed { index, it -> + return chapterList.mapIndexed { index, it -> - val dbChapter = dbChapterMap.getValue(it.url) + val dbChapter = dbChapterMap.getValue(it.url) - ChapterDataClass( - it.url, - it.name, - it.date_upload, - it.chapter_number, - it.scanlator, - mangaId, + ChapterDataClass( + it.url, + it.name, + it.date_upload, + it.chapter_number, + it.scanlator, + mangaId, - dbChapter[ChapterTable.isRead], - dbChapter[ChapterTable.isBookmarked], - dbChapter[ChapterTable.lastPageRead], + dbChapter[ChapterTable.isRead], + dbChapter[ChapterTable.isBookmarked], + dbChapter[ChapterTable.lastPageRead], - chapterCount - index, - ) - } + chapterCount - index, + chapterList.size + ) } } @@ -125,9 +134,9 @@ object Chapter { val chapterEntry = transaction { ChapterTable.select { (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) - }.firstOrNull()!! + }.first() } - val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val pageList = source.fetchPageList( 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 02fc24d..3a5bbab 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt @@ -159,7 +159,7 @@ object Extension { it[this.classFQName] = className } - val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!![ExtensionTable.id].value + val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first()[ExtensionTable.id].value sources.forEach { httpSource -> SourceTable.insert { @@ -195,7 +195,7 @@ object Extension { fun uninstallExtension(pkgName: String) { logger.debug("Uninstalling $pkgName") - val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! } + val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first() } val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk") val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar" transaction { @@ -234,7 +234,7 @@ object Extension { } suspend fun getExtensionIcon(apkName: String): Pair { - val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl] + val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl] val saveDir = "${applicationDirs.extensionsRoot}/icon" 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 527974c..1602ad4 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt @@ -37,7 +37,7 @@ object Manga { } suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { - var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } return if (mangaEntry[MangaTable.initialized] && !onlineFetch) { MangaDataClass( @@ -78,14 +78,14 @@ object Manga { it[MangaTable.description] = truncate(fetchedManga.description, 4096) it[MangaTable.genre] = fetchedManga.genre it[MangaTable.status] = fetchedManga.status - if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) + if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty()) it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url } } clearMangaThumbnail(mangaId) - mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } MangaDataClass( mangaId, @@ -117,7 +117,7 @@ object Manga { return getCachedImageResponse(saveDir, fileName) { getManga(mangaId) // make sure is initialized - val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val sourceId = mangaEntry[MangaTable.sourceReference] val source = getHttpSource(sourceId) @@ -130,7 +130,7 @@ object Manga { } } - suspend fun clearMangaThumbnail(mangaId: Int) { + private fun clearMangaThumbnail(mangaId: Int) { val saveDir = applicationDirs.thumbnailsRoot val fileName = mangaId.toString() 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 94a632a..59c2bcb 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/MangaList.kt @@ -40,7 +40,7 @@ object MangaList { val mangasPage = this val mangaList = transaction { return@transaction mangasPage.mangas.map { manga -> - var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() + val mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() if (mangaEntry == null) { // create manga entry val mangaId = MangaTable.insertAndGetId { it[url] = manga.url 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 712f5ab..a03ac30 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt @@ -40,16 +40,16 @@ object Page { } suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair { - val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val chapterEntry = transaction { ChapterTable.select { (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) - }.firstOrNull()!! + }.first() } val chapterId = chapterEntry[ChapterTable.id].value - val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! } + val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.first() } val tachiPage = Page( pageEntry[PageTable.index], @@ -78,11 +78,11 @@ object Page { // TODO: rewrite this to match tachiyomi private val applicationDirs by DI.global.instance() fun getChapterDir(mangaId: Int, chapterId: Int): String { - val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val sourceId = mangaEntry[MangaTable.sourceReference] val source = getHttpSource(sourceId) - val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! } - val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! } + val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.first() } + val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() } val chapterDir = when { chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}" diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt index 0366dcf..a07ad3a 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt @@ -198,7 +198,7 @@ object LegacyBackupImport : LegacyBackupBase() { it[description] = fetchedManga.description it[genre] = fetchedManga.genre it[status] = fetchedManga.status - if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) + if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty()) it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url } } 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 index e8eb2c0..d21a011 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt @@ -10,7 +10,6 @@ package ir.armor.tachidesk.impl.util 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 @@ -19,12 +18,12 @@ import java.nio.file.Paths object CachedImageResponse { private fun pathToInputStream(path: String): InputStream { - return BufferedInputStream(FileInputStream(path)) + return FileInputStream(path).buffered() } private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? { val target = "$fileName." - File(directoryPath).listFiles().forEach { file -> + File(directoryPath).listFiles().orEmpty().forEach { file -> if (file.name.startsWith(target)) return "$directoryPath/${file.name}" } @@ -57,16 +56,13 @@ object CachedImageResponse { } } } - return Pair( - pathToInputStream(fullPath), - contentType - ) + return pathToInputStream(fullPath) to contentType } else { throw Exception("request error! ${response.code}") } } - suspend fun clearCachedImage(saveDir: String, fileName: String) { + fun clearCachedImage(saveDir: String, fileName: String) { val cachedFile = findFileNameStartingWith(saveDir, fileName) cachedFile?.also { File(it).delete() diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/GetHttpSource.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/GetHttpSource.kt index 7c93a08..f309343 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/GetHttpSource.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/GetHttpSource.kt @@ -32,12 +32,12 @@ object GetHttpSource { } val sourceRecord = transaction { - SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! + SourceTable.select { SourceTable.id eq sourceId }.first() } val extensionId = sourceRecord[SourceTable.extension] val extensionRecord = transaction { - ExtensionTable.select { ExtensionTable.id eq extensionId }.firstOrNull()!! + ExtensionTable.select { ExtensionTable.id eq extensionId }.first() } val apkName = extensionRecord[ExtensionTable.apkName] diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/OkHttp.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/OkHttp.kt index 825cd06..5ee5e6e 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/OkHttp.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/OkHttp.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.Call import okhttp3.Callback import okhttp3.Response +import okhttp3.internal.closeQuietly import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -26,7 +27,9 @@ suspend fun Call.await(): Response { return } - continuation.resume(response) + continuation.resume(response) { + response.body?.closeQuietly() + } } override fun onFailure(call: Call, e: IOException) { diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/PackageTools.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/PackageTools.kt index 44ea565..5382e2c 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/PackageTools.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/PackageTools.kt @@ -71,12 +71,14 @@ object PackageTools { if (handler.hasException()) { val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt") logger.error( - "Detail Error Information in File $errorFile\n" + - "Please report this file to one of following link if possible (any one).\n" + - " https://sourceforge.net/p/dex2jar/tickets/\n" + - " https://bitbucket.org/pxb1988/dex2jar/issues\n" + - " https://github.com/pxb1988/dex2jar/issues\n" + - " dex2jar@googlegroups.com" + """ + Detail Error Information in File $errorFile + Please report this file to one of following link if possible (any one). + https://sourceforge.net/p/dex2jar/tickets/ + https://bitbucket.org/pxb1988/dex2jar/issues + https://github.com/pxb1988/dex2jar/issues + dex2jar@googlegroups.com + """.trimIndent() ) handler.dump(errorFile, emptyArray()) } @@ -98,18 +100,21 @@ object PackageTools { applicationInfo.metaData = Bundle().apply { val appTag = doc.getElementsByTagName("application").item(0) - appTag?.childNodes?.toList()?.filter { - it.nodeType == Node.ELEMENT_NODE - }?.map { - it as Element - }?.filter { - it.tagName == "meta-data" - }?.map { - putString( - it.attributes.getNamedItem("android:name").nodeValue, - it.attributes.getNamedItem("android:value").nodeValue - ) - } + appTag?.childNodes?.toList() + .orEmpty() + .asSequence() + .filter { + it.nodeType == Node.ELEMENT_NODE + }.map { + it as Element + }.filter { + it.tagName == "meta-data" + }.forEach { + putString( + it.attributes.getNamedItem("android:name").nodeValue, + it.attributes.getNamedItem("android:value").nodeValue + ) + } } signatures = ( 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 c0d824c..0660782 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 @@ -25,7 +25,7 @@ data class ChapterDataClass( val lastPageRead: Int, /** this chapter's index, starts with 1 */ - val index: Int? = null, + val index: Int, /** total chapter count, used to calculate if there's a next and prev chapter */ val chapterCount: 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 7b0a1a4..b9344eb 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt @@ -96,7 +96,10 @@ object JavalinSetup { logger.error("NullPointerException while handling the request", e) ctx.status(404) } - + app.exception(NoSuchElementException::class.java) { e, ctx -> + logger.error("NoSuchElementException while handling the request", e) + ctx.status(404) + } app.exception(IOException::class.java) { e, ctx -> logger.error("IOException while handling the request", e) ctx.status(500) @@ -257,7 +260,7 @@ object JavalinSetup { app.get("/api/v1/manga/:mangaId/chapters") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() - val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean() + val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() ctx.json(future { getChapterList(mangaId, onlineFetch) }) }