refactor and more

This commit is contained in:
Aria Moradi 2021-04-02 02:51:12 +04:30
parent f983f0e359
commit 035105adf0
13 changed files with 149 additions and 165 deletions

View File

@ -9,24 +9,22 @@ 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.impl.util.awaitSingle
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.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
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
object Chapter { object Chapter {
/** get chapter list when showing a manga */
suspend 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())
@ -44,7 +42,7 @@ object Chapter {
chapterList.reversed().forEachIndexed { index, fetchedChapter -> chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) { if (chapterEntry == null) {
ChapterTable.insertAndGetId { ChapterTable.insert {
it[url] = fetchedChapter.url it[url] = fetchedChapter.url
it[name] = fetchedChapter.name it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload 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() } val dbChapterCount = transaction { ChapterTable.selectAll().count() }
if (dbChapterCount > chapterCount) { // we got some clean up due if (dbChapterCount > chapterCount) { // we got some clean up due
// TODO: delete orphan chapters // TODO: delete orphan chapters
} }
chapterList.mapIndexed { index, it -> chapterList.map { it ->
ChapterDataClass( ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url, it.url,
it.name, it.name,
it.date_upload, it.date_upload,
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId, 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 { suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
var chapterEntry: ResultRow? = null val chapterEntry = transaction {
var source: HttpSource? = null ChapterTable.select {
var sChapter: SChapter? = null (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
transaction {
chapterEntry = ChapterTable.select {
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
}.firstOrNull()!! }.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( val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
sChapter!! val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).awaitSingle() ).awaitSingle()
return transaction { val chapterId = chapterEntry[ChapterTable.id].value
val chapterRow = chapterEntry!! val chapterCount = transaction { ChapterTable.selectAll().count() }
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()
)
// update page list for this chapter
transaction {
pageList.forEach { page -> pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() } val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
if (pageEntry == null) { if (pageEntry == null) {
transaction { PageTable.insert {
PageTable.insert { it[index] = page.index
it[index] = page.index it[url] = page.url
it[url] = page.url it[imageUrl] = page.imageUrl
it[imageUrl] = page.imageUrl it[chapter] = chapterId
it[this.chapter] = chapterId
}
} }
} else { } else {
transaction { PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) { it[url] = page.url
it[url] = page.url it[imageUrl] = page.imageUrl
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()
)
} }
} }

View File

@ -17,10 +17,11 @@ import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
import ir.armor.tachidesk.impl.util.APKExtractor 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.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 ir.armor.tachidesk.util.await
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
@ -136,7 +137,7 @@ object Extension {
} }
is SourceFactory -> { // theme source or multi lang is SourceFactory -> { // theme source or multi lang
transaction { transaction {
instance.createSources().forEachIndexed { index, source -> instance.createSources().forEach { source ->
val httpSource = source as HttpSource val httpSource = source as HttpSource
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) { if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert { SourceTable.insert {

View File

@ -9,15 +9,16 @@ 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
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.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

View File

@ -8,8 +8,8 @@ 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.impl.util.awaitSingle
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
import ir.armor.tachidesk.model.dataclass.MangaDataClass import ir.armor.tachidesk.model.dataclass.MangaDataClass

View File

@ -9,8 +9,9 @@ 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.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.awaitSingle
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

View File

@ -7,9 +7,9 @@ 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.impl.util.awaitSingle
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
object Search { object Search {

View File

@ -175,8 +175,8 @@ object APKExtractor {
return compXmlStringAt(xml, strOff) return compXmlStringAt(xml, strOff)
} }
var spaces = " " private var spaces = " "
fun prtIndent(indent: Int, str: String) { private fun prtIndent(indent: Int, str: String) {
logger.debug(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str) logger.debug(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str)
} }

View File

@ -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<InputStream, String> {
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}")
}
}
}

View File

@ -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<InputStream, String> {
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}")
}
}

View File

@ -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 kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call

View File

@ -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.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 kotlinx.coroutines.suspendCancellableCoroutine
import rx.Emitter
import rx.Observable import rx.Observable
import rx.Subscriber import rx.Subscriber
import rx.Subscription import rx.Subscription
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException 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. * Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY.
*/ */
@ -56,5 +58,5 @@ private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutin
) )
} }
internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) = private fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
invokeOnCancellation { sub.unsubscribe() } invokeOnCancellation { sub.unsubscribe() }

View File

@ -8,14 +8,19 @@ package ir.armor.tachidesk.model.dataclass
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class ChapterDataClass( data class ChapterDataClass(
val id: Int,
val url: String, val url: String,
val name: String, val name: String,
val date_upload: Long, val date_upload: Long,
val chapter_number: Float, val chapter_number: Float,
val scanlator: String?, val scanlator: String?,
val mangaId: Int, 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, val pageCount: Int? = null,
) )

View File

@ -33,7 +33,6 @@ 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.future.future import kotlinx.coroutines.future.future
import mu.KotlinLogging import mu.KotlinLogging
@ -198,11 +197,13 @@ object JavalinSetup {
ctx.status(200) ctx.status(200)
} }
// get chapter list when showing a manga
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(scope.future { getChapterList(mangaId) }) 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 -> 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()