refactor and more
This commit is contained in:
@@ -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<ChapterDataClass> {
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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.suspendCancellableCoroutine
|
||||
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.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
|
||||
invokeOnCancellation { sub.unsubscribe() }
|
||||
Reference in New Issue
Block a user