Support EPUB and TXT downloads from Kavita and BookOasis
Build & Test / Build & Test App (push) Has been cancelled
Build & Test / Build & Test App (push) Has been cancelled
This commit is contained in:
@@ -376,8 +376,10 @@ class DownloadCache(
|
|||||||
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
|
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
|
||||||
// Folder of images
|
// Folder of images
|
||||||
it.isDirectory -> it.name
|
it.isDirectory -> it.name
|
||||||
// CBZ files
|
// Archived or original document files
|
||||||
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
|
it.isFile && it.extension?.lowercase()?.let { extension ->
|
||||||
|
extension == "cbz" || extension in DOCUMENT_DOWNLOAD_EXTENSIONS
|
||||||
|
} == true -> it.nameWithoutExtension
|
||||||
// Anything else is irrelevant
|
// Anything else is irrelevant
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,8 +405,12 @@ class DownloadManager(
|
|||||||
.firstOrNull() ?: return
|
.firstOrNull() ?: return
|
||||||
|
|
||||||
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||||
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
if (oldDownload.isFile) {
|
||||||
newName += ".cbz"
|
oldDownload.extension?.lowercase()?.let { extension ->
|
||||||
|
if (extension == "cbz" || extension in DOCUMENT_DOWNLOAD_EXTENSIONS) {
|
||||||
|
newName += ".$extension"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldDownload.name == newName) return
|
if (oldDownload.name == newName) return
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import tachiyomi.domain.storage.service.StorageManager
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
internal val DOCUMENT_DOWNLOAD_EXTENSIONS = setOf("epub", "txt")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to provide the directories where the downloads should be saved.
|
* This class is used to provide the directories where the downloads should be saved.
|
||||||
* It uses the following path scheme: /<root downloads dir>/<source name>/<manga>/<chapter>
|
* It uses the following path scheme: /<root downloads dir>/<source name>/<manga>/<chapter>
|
||||||
@@ -250,11 +252,17 @@ class DownloadProvider(
|
|||||||
add(chapterDirName)
|
add(chapterDirName)
|
||||||
// Archived chapters
|
// Archived chapters
|
||||||
add("$chapterDirName.cbz")
|
add("$chapterDirName.cbz")
|
||||||
|
DOCUMENT_DOWNLOAD_EXTENSIONS.forEach { extension ->
|
||||||
|
add("$chapterDirName.$extension")
|
||||||
|
}
|
||||||
|
|
||||||
// any legacy names
|
// any legacy names
|
||||||
legacyChapterDirNames.forEach {
|
legacyChapterDirNames.forEach {
|
||||||
add(it)
|
add(it)
|
||||||
add("$it.cbz")
|
add("$it.cbz")
|
||||||
|
DOCUMENT_DOWNLOAD_EXTENSIONS.forEach { extension ->
|
||||||
|
add("$it.$extension")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,16 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
|
import eu.kanade.tachiyomi.source.bookoasis.bookOasisDocumentDownloadRequest
|
||||||
|
import eu.kanade.tachiyomi.source.bookoasis.isBookOasisSource
|
||||||
|
import eu.kanade.tachiyomi.source.bookoasis.toBookOasisChapterRef
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.detectKavitaDocumentFormat
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.isKavitaSource
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.kavitaDocumentDownloadRequest
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.toKavitaChapterId
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||||
@@ -44,6 +52,7 @@ import kotlinx.coroutines.supervisorScope
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.core.archive.ZipWriter
|
import mihon.core.archive.ZipWriter
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
@@ -339,9 +348,15 @@ class Downloader(
|
|||||||
download.chapter.scanlator,
|
download.chapter.scanlator,
|
||||||
download.chapter.url,
|
download.chapter.url,
|
||||||
)
|
)
|
||||||
|
try {
|
||||||
|
val remoteDocument = findRemoteDocument(download)
|
||||||
|
if (remoteDocument != null) {
|
||||||
|
downloadDocument(download, mangaDir, chapterDirname, remoteDocument)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
||||||
|
|
||||||
try {
|
|
||||||
// If the page list already exists, start from the file
|
// If the page list already exists, start from the file
|
||||||
val pageList = download.pages ?: run {
|
val pageList = download.pages ?: run {
|
||||||
// Otherwise, pull page list from network and add them to download object
|
// Otherwise, pull page list from network and add them to download object
|
||||||
@@ -415,6 +430,84 @@ class Downloader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun findRemoteDocument(download: Download): RemoteDocument? {
|
||||||
|
val source = download.source
|
||||||
|
return when {
|
||||||
|
source.isBookOasisSource() -> download.chapter.url
|
||||||
|
.toBookOasisChapterRef()
|
||||||
|
?.takeIf { it.format in DOCUMENT_DOWNLOAD_EXTENSIONS }
|
||||||
|
?.let { ref -> RemoteDocument(ref.format, source.bookOasisDocumentDownloadRequest(ref)) }
|
||||||
|
source.isKavitaSource() -> download.chapter.url
|
||||||
|
.toKavitaChapterId()
|
||||||
|
?.let { chapterId ->
|
||||||
|
source.detectKavitaDocumentFormat(chapterId)?.let { format ->
|
||||||
|
RemoteDocument(format, source.kavitaDocumentDownloadRequest(chapterId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun downloadDocument(
|
||||||
|
download: Download,
|
||||||
|
mangaDir: UniFile,
|
||||||
|
chapterDirname: String,
|
||||||
|
document: RemoteDocument,
|
||||||
|
) {
|
||||||
|
val targetName = "$chapterDirname.${document.extension}"
|
||||||
|
mangaDir.findFile(targetName)?.delete()
|
||||||
|
mangaDir.findFile("$targetName.tmp")?.delete()
|
||||||
|
val temp = mangaDir.createFile("$targetName.tmp")
|
||||||
|
?: error("Failed to create temporary document file")
|
||||||
|
val page = Page(0).apply {
|
||||||
|
status = Page.State.DownloadImage
|
||||||
|
}
|
||||||
|
download.pages = listOf(page)
|
||||||
|
download.status = Download.State.DOWNLOADING
|
||||||
|
notifier.onProgressChange(download)
|
||||||
|
|
||||||
|
try {
|
||||||
|
download.source.client.newCall(document.request).await().use { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Document download failed: HTTP ${response.code}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
temp.openOutputStream().use { output ->
|
||||||
|
response.body.byteStream().use { input -> input.copyTo(output) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.length() <= 0L) {
|
||||||
|
throw IllegalStateException("Document download returned an empty file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!temp.renameTo(targetName)) {
|
||||||
|
val target = mangaDir.createFile(targetName)
|
||||||
|
?: error("Failed to create downloaded document file")
|
||||||
|
temp.openInputStream().use { input ->
|
||||||
|
target.openOutputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
temp.delete()
|
||||||
|
}
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
temp.delete()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
val target = mangaDir.findFile(targetName)
|
||||||
|
?: error("Downloaded document file is missing")
|
||||||
|
page.uri = target.uri
|
||||||
|
page.status = Page.State.Ready
|
||||||
|
page.progress = 100
|
||||||
|
notifier.onProgressChange(download)
|
||||||
|
cache.addChapter(chapterDirname, mangaDir, download.manga)
|
||||||
|
DiskUtil.createNoMediaFile(mangaDir, context)
|
||||||
|
download.status = Download.State.DOWNLOADED
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the image from the filesystem if it exists or downloads it otherwise.
|
* Gets the image from the filesystem if it exists or downloads it otherwise.
|
||||||
*
|
*
|
||||||
@@ -740,5 +833,10 @@ class Downloader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class RemoteDocument(
|
||||||
|
val extension: String,
|
||||||
|
val request: Request,
|
||||||
|
)
|
||||||
|
|
||||||
// Arbitrary minimum required space to start a download: 200 MB
|
// Arbitrary minimum required space to start a download: 200 MB
|
||||||
private const val MIN_DISK_SPACE = 200L * 1024 * 1024
|
private const val MIN_DISK_SPACE = 200L * 1024 * 1024
|
||||||
|
|||||||
@@ -21,6 +21,22 @@ internal data class BookOasisChapterRef(
|
|||||||
val format: String,
|
val format: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun HttpSource.bookOasisDocumentDownloadRequest(ref: BookOasisChapterRef): Request {
|
||||||
|
val downloadUrl = buildString {
|
||||||
|
append(baseUrl.trimEnd('/'))
|
||||||
|
append("/api/media/books/")
|
||||||
|
append(ref.bookId)
|
||||||
|
append("/download?type=")
|
||||||
|
append(ref.dbType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Request.Builder()
|
||||||
|
.url(downloadUrl)
|
||||||
|
.headers(headers)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
internal fun Source.isBookOasisSource(): Boolean {
|
internal fun Source.isBookOasisSource(): Boolean {
|
||||||
return this::class.java.name.startsWith(BOOKOASIS_PACKAGE_PREFIX)
|
return this::class.java.name.startsWith(BOOKOASIS_PACKAGE_PREFIX)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,56 @@
|
|||||||
package eu.kanade.tachiyomi.source.kavita
|
package eu.kanade.tachiyomi.source.kavita
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
private const val KAVITA_PACKAGE_PREFIX = "eu.kanade.tachiyomi.extension.all.kavita."
|
private const val KAVITA_PACKAGE_PREFIX = "eu.kanade.tachiyomi.extension.all.kavita."
|
||||||
|
|
||||||
internal fun Source.isKavitaSource(): Boolean =
|
internal fun Source.isKavitaSource(): Boolean =
|
||||||
this::class.java.name.startsWith(KAVITA_PACKAGE_PREFIX)
|
this::class.java.name.startsWith(KAVITA_PACKAGE_PREFIX)
|
||||||
|
|
||||||
|
internal suspend fun HttpSource.detectKavitaDocumentFormat(chapterId: Int): String? {
|
||||||
|
val apiBase = baseUrl.trimEnd('/') + "/api"
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$apiBase/Chapter?chapterId=$chapterId")
|
||||||
|
.headers(headers)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).await().use { response ->
|
||||||
|
if (!response.isSuccessful) return null
|
||||||
|
|
||||||
|
val body = response.body.string()
|
||||||
|
if (body.isBlank()) return null
|
||||||
|
|
||||||
|
val files = JSONObject(body).optJSONArray("files") ?: return null
|
||||||
|
for (i in 0 until files.length()) {
|
||||||
|
val file = files.optJSONObject(i) ?: continue
|
||||||
|
val extension = file.optString("extension")
|
||||||
|
.ifBlank { file.optString("filePath") }
|
||||||
|
.substringAfterLast('.')
|
||||||
|
.trim()
|
||||||
|
.trimStart('.')
|
||||||
|
.lowercase(Locale.ROOT)
|
||||||
|
if (extension == "epub" || extension == "txt") return extension
|
||||||
|
if (file.optInt("format", -1) == 3) return "epub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun HttpSource.kavitaDocumentDownloadRequest(chapterId: Int): Request {
|
||||||
|
val apiBase = baseUrl.trimEnd('/') + "/api"
|
||||||
|
return Request.Builder()
|
||||||
|
.url("$apiBase/Download/chapter?chapterId=$chapterId")
|
||||||
|
.headers(headers)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kavita extension chapter URLs are normally /Chapter/{id}. Keep compatibility
|
* Kavita extension chapter URLs are normally /Chapter/{id}. Keep compatibility
|
||||||
* with older/alternate forms used by reading lists and previous releases.
|
* with older/alternate forms used by reading lists and previous releases.
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import android.content.Context
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.bookoasis.BookOasisChapterRef
|
import eu.kanade.tachiyomi.source.bookoasis.BookOasisChapterRef
|
||||||
|
import eu.kanade.tachiyomi.source.bookoasis.bookOasisDocumentDownloadRequest
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
||||||
import mihon.core.archive.epubReader
|
import mihon.core.archive.epubReader
|
||||||
import okhttp3.Request
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@@ -68,19 +68,7 @@ internal class BookOasisFilePageLoader(
|
|||||||
val temp = File(cacheDir, "${target.name}.part")
|
val temp = File(cacheDir, "${target.name}.part")
|
||||||
if (temp.exists()) temp.delete()
|
if (temp.exists()) temp.delete()
|
||||||
|
|
||||||
val downloadUrl = buildString {
|
val request = source.bookOasisDocumentDownloadRequest(ref)
|
||||||
append(source.baseUrl.trimEnd('/'))
|
|
||||||
append("/api/media/books/")
|
|
||||||
append(ref.bookId)
|
|
||||||
append("/download?type=")
|
|
||||||
append(ref.dbType)
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(downloadUrl)
|
|
||||||
.headers(source.headers)
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
source.client.newCall(request).await().use { response ->
|
source.client.newCall(request).await().use { response ->
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import mihon.core.archive.archiveReader
|
import mihon.core.archive.archiveReader
|
||||||
|
import mihon.core.archive.epubReader
|
||||||
|
import tachiyomi.core.common.storage.extension
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,9 +30,12 @@ internal class DownloadPageLoader(
|
|||||||
|
|
||||||
private val context: Context by injectLazy()
|
private val context: Context by injectLazy()
|
||||||
|
|
||||||
private var archivePageLoader: ArchivePageLoader? = null
|
private var pageLoader: PageLoader? = null
|
||||||
|
private val readerPreferences: ReaderPreferences by injectLazy()
|
||||||
|
|
||||||
override var isLocal: Boolean = true
|
override var isLocal: Boolean = true
|
||||||
|
override val usesTextReaderStyle: Boolean
|
||||||
|
get() = pageLoader?.usesTextReaderStyle == true
|
||||||
|
|
||||||
override suspend fun getPages(): List<ReaderPage> {
|
override suspend fun getPages(): List<ReaderPage> {
|
||||||
val dbChapter = chapter.chapter
|
val dbChapter = chapter.chapter
|
||||||
@@ -41,7 +47,11 @@ internal class DownloadPageLoader(
|
|||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
return if (chapterPath?.isFile == true) {
|
return if (chapterPath?.isFile == true) {
|
||||||
getPagesFromArchive(chapterPath)
|
when (chapterPath.extension?.lowercase()) {
|
||||||
|
"epub" -> getPagesFromEpub(chapterPath)
|
||||||
|
"txt" -> getPagesFromText(chapterPath)
|
||||||
|
else -> getPagesFromArchive(chapterPath)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
getPagesFromDirectory()
|
getPagesFromDirectory()
|
||||||
}
|
}
|
||||||
@@ -49,11 +59,30 @@ internal class DownloadPageLoader(
|
|||||||
|
|
||||||
override fun recycle() {
|
override fun recycle() {
|
||||||
super.recycle()
|
super.recycle()
|
||||||
archivePageLoader?.recycle()
|
pageLoader?.recycle()
|
||||||
|
pageLoader = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPagesFromArchive(file: UniFile): List<ReaderPage> {
|
private suspend fun getPagesFromArchive(file: UniFile): List<ReaderPage> {
|
||||||
val loader = ArchivePageLoader(file.archiveReader(context)).also { archivePageLoader = it }
|
val loader = ArchivePageLoader(file.archiveReader(context)).also { pageLoader = it }
|
||||||
|
return loader.getPages()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getPagesFromEpub(file: UniFile): List<ReaderPage> {
|
||||||
|
val loader = EpubPageLoader(
|
||||||
|
file.epubReader(context),
|
||||||
|
readerPreferences.textReaderStyle(context),
|
||||||
|
context.cacheDir,
|
||||||
|
).also { pageLoader = it }
|
||||||
|
return loader.getPages()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getPagesFromText(file: UniFile): List<ReaderPage> {
|
||||||
|
val loader = TextPageLoader(
|
||||||
|
file,
|
||||||
|
readerPreferences.textReaderStyle(context),
|
||||||
|
context.cacheDir,
|
||||||
|
).also { pageLoader = it }
|
||||||
return loader.getPages()
|
return loader.getPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +98,6 @@ internal class DownloadPageLoader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadPage(page: ReaderPage) {
|
override suspend fun loadPage(page: ReaderPage) {
|
||||||
archivePageLoader?.loadPage(page)
|
pageLoader?.loadPage(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ import android.content.Context
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.detectKavitaDocumentFormat
|
||||||
|
import eu.kanade.tachiyomi.source.kavita.kavitaDocumentDownloadRequest
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
||||||
import mihon.core.archive.epubReader
|
import mihon.core.archive.epubReader
|
||||||
import okhttp3.Request
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compatibility loader for Kavita document chapters.
|
* Compatibility loader for Kavita document chapters.
|
||||||
@@ -42,7 +41,7 @@ internal class KavitaCompatPageLoader(
|
|||||||
override suspend fun getPages(): List<ReaderPage> {
|
override suspend fun getPages(): List<ReaderPage> {
|
||||||
check(!isRecycled)
|
check(!isRecycled)
|
||||||
|
|
||||||
val format = detectDocumentFormat()
|
val format = source.detectKavitaDocumentFormat(chapterId)
|
||||||
val loader = when (format) {
|
val loader = when (format) {
|
||||||
"epub", "txt" -> createDocumentLoader(format)
|
"epub", "txt" -> createDocumentLoader(format)
|
||||||
else -> HttpPageLoader(chapter, source, chapterCache)
|
else -> HttpPageLoader(chapter, source, chapterCache)
|
||||||
@@ -67,51 +66,6 @@ internal class KavitaCompatPageLoader(
|
|||||||
super.recycle()
|
super.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun detectDocumentFormat(): String? {
|
|
||||||
val apiBase = source.baseUrl.trimEnd('/') + "/api"
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$apiBase/Chapter?chapterId=$chapterId")
|
|
||||||
.headers(source.headers)
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
source.client.newCall(request).await().use { response ->
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
// Preserve the extension's normal behavior when chapter metadata
|
|
||||||
// cannot be queried. It may still be readable as image content.
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val body = response.body.string()
|
|
||||||
if (body.isBlank()) return null
|
|
||||||
|
|
||||||
val chapterJson = JSONObject(body)
|
|
||||||
val files = chapterJson.optJSONArray("files") ?: return null
|
|
||||||
for (i in 0 until files.length()) {
|
|
||||||
val file = files.optJSONObject(i) ?: continue
|
|
||||||
val extension = normalizeExtension(
|
|
||||||
file.optString("extension").ifBlank { file.optString("filePath") },
|
|
||||||
)
|
|
||||||
if (extension == "epub" || extension == "txt") {
|
|
||||||
return extension
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current Kavita format enum: EPUB == 3. TXT has no dedicated
|
|
||||||
// enum value in upstream Kavita, so it is detected by extension.
|
|
||||||
if (file.optInt("format", -1) == 3) {
|
|
||||||
return "epub"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun normalizeExtension(value: String): String =
|
|
||||||
value.substringAfterLast('.', value)
|
|
||||||
.trim()
|
|
||||||
.trimStart('.')
|
|
||||||
.lowercase(Locale.ROOT)
|
|
||||||
|
|
||||||
private suspend fun createDocumentLoader(format: String): PageLoader {
|
private suspend fun createDocumentLoader(format: String): PageLoader {
|
||||||
val file = getOrDownloadDocument(format)
|
val file = getOrDownloadDocument(format)
|
||||||
val uniFile = UniFile.fromFile(file)
|
val uniFile = UniFile.fromFile(file)
|
||||||
@@ -132,12 +86,7 @@ internal class KavitaCompatPageLoader(
|
|||||||
val temp = File(cacheDir, "${target.name}.part")
|
val temp = File(cacheDir, "${target.name}.part")
|
||||||
if (temp.exists()) temp.delete()
|
if (temp.exists()) temp.delete()
|
||||||
|
|
||||||
val apiBase = source.baseUrl.trimEnd('/') + "/api"
|
val request = source.kavitaDocumentDownloadRequest(chapterId)
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$apiBase/Download/chapter?chapterId=$chapterId")
|
|
||||||
.headers(source.headers)
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
source.client.newCall(request).await().use { response ->
|
source.client.newCall(request).await().use { response ->
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
|
|||||||
Reference in New Issue
Block a user