This commit is contained in:
@@ -117,6 +117,37 @@ class DownloadProvider(
|
|||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a downloaded EPUB/TXT document for a chapter if it exists.
|
||||||
|
*
|
||||||
|
* Document files are checked explicitly before the generic chapter path so
|
||||||
|
* a stale image directory with the same chapter name cannot shadow them.
|
||||||
|
*/
|
||||||
|
fun findChapterDocument(
|
||||||
|
chapterName: String,
|
||||||
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
|
mangaTitle: String,
|
||||||
|
source: Source,
|
||||||
|
): UniFile? {
|
||||||
|
val mangaDir = findMangaDir(mangaTitle, source) ?: return null
|
||||||
|
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
|
||||||
|
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
|
||||||
|
|
||||||
|
return buildList {
|
||||||
|
DOCUMENT_DOWNLOAD_EXTENSIONS.forEach { extension ->
|
||||||
|
add("$chapterDirName.$extension")
|
||||||
|
}
|
||||||
|
legacyChapterDirNames.forEach { legacyName ->
|
||||||
|
DOCUMENT_DOWNLOAD_EXTENSIONS.forEach { extension ->
|
||||||
|
add("$legacyName.$extension")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.asSequence()
|
||||||
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
|
.firstOrNull { it.isFile }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of downloaded directories for the chapters that exist.
|
* Returns a list of downloaded directories for the chapters that exist.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -130,6 +130,33 @@ class ChapterLoader(
|
|||||||
)
|
)
|
||||||
val bookOasisRef = dbChapter.url.toBookOasisChapterRef()
|
val bookOasisRef = dbChapter.url.toBookOasisChapterRef()
|
||||||
val kavitaChapterId = dbChapter.url.toKavitaChapterId()
|
val kavitaChapterId = dbChapter.url.toKavitaChapterId()
|
||||||
|
val httpSource = source as? HttpSource
|
||||||
|
val documentFallback: (() -> PageLoader)? = when {
|
||||||
|
httpSource != null && httpSource.isBookOasisSource() && bookOasisRef != null &&
|
||||||
|
(bookOasisRef.format == "epub" || bookOasisRef.format == "txt") -> {
|
||||||
|
{
|
||||||
|
BookOasisFilePageLoader(
|
||||||
|
context,
|
||||||
|
httpSource,
|
||||||
|
bookOasisRef,
|
||||||
|
readerPreferences.textReaderStyle(context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpSource != null && httpSource.isKavitaSource() && kavitaChapterId != null -> {
|
||||||
|
{
|
||||||
|
KavitaCompatPageLoader(
|
||||||
|
context = context,
|
||||||
|
chapter = chapter,
|
||||||
|
source = httpSource,
|
||||||
|
chapterCache = chapterCache,
|
||||||
|
chapterId = kavitaChapterId,
|
||||||
|
textStyle = readerPreferences.textReaderStyle(context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
isDownloaded -> DownloadPageLoader(
|
isDownloaded -> DownloadPageLoader(
|
||||||
@@ -138,6 +165,7 @@ class ChapterLoader(
|
|||||||
source,
|
source,
|
||||||
downloadManager,
|
downloadManager,
|
||||||
downloadProvider,
|
downloadProvider,
|
||||||
|
documentFallback,
|
||||||
)
|
)
|
||||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||||
when (format) {
|
when (format) {
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
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 kotlinx.coroutines.CancellationException
|
||||||
import mihon.core.archive.archiveReader
|
import mihon.core.archive.archiveReader
|
||||||
import mihon.core.archive.epubReader
|
import mihon.core.archive.epubReader
|
||||||
import tachiyomi.core.common.storage.extension
|
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 eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to load a chapter from the downloaded chapters.
|
* Loader used to load a chapter from the downloaded chapters.
|
||||||
@@ -26,6 +28,7 @@ internal class DownloadPageLoader(
|
|||||||
private val source: Source,
|
private val source: Source,
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
private val downloadProvider: DownloadProvider,
|
private val downloadProvider: DownloadProvider,
|
||||||
|
private val documentFallback: (() -> PageLoader)? = null,
|
||||||
) : PageLoader() {
|
) : PageLoader() {
|
||||||
|
|
||||||
private val context: Context by injectLazy()
|
private val context: Context by injectLazy()
|
||||||
@@ -35,9 +38,35 @@ internal class DownloadPageLoader(
|
|||||||
|
|
||||||
override var isLocal: Boolean = true
|
override var isLocal: Boolean = true
|
||||||
override val usesTextReaderStyle: Boolean
|
override val usesTextReaderStyle: Boolean
|
||||||
get() = pageLoader?.usesTextReaderStyle == true
|
get() = pageLoader?.usesTextReaderStyle == true || findDownloadedDocument() != null
|
||||||
|
|
||||||
override suspend fun getPages(): List<ReaderPage> {
|
override suspend fun getPages(): List<ReaderPage> {
|
||||||
|
check(!isRecycled)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val pages = getDownloadedPages()
|
||||||
|
if (pages.isNotEmpty() || documentFallback == null) {
|
||||||
|
pages
|
||||||
|
} else {
|
||||||
|
getFallbackPages()
|
||||||
|
}
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
if (error is CancellationException || documentFallback == null) throw error
|
||||||
|
getFallbackPages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getDownloadedPages(): List<ReaderPage> {
|
||||||
|
val downloadedDocument = findDownloadedDocument()
|
||||||
|
if (downloadedDocument != null) {
|
||||||
|
val readableDocument = prepareDownloadedDocument(downloadedDocument)
|
||||||
|
return when (readableDocument.extension?.lowercase()) {
|
||||||
|
"epub" -> getPagesFromEpub(readableDocument)
|
||||||
|
"txt" -> getPagesFromText(readableDocument)
|
||||||
|
else -> error("Unsupported downloaded document: ${downloadedDocument.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val dbChapter = chapter.chapter
|
val dbChapter = chapter.chapter
|
||||||
val chapterPath = downloadProvider.findChapterDir(
|
val chapterPath = downloadProvider.findChapterDir(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
@@ -46,17 +75,79 @@ internal class DownloadPageLoader(
|
|||||||
manga.title,
|
manga.title,
|
||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (chapterPath?.isFile == true) {
|
return if (chapterPath?.isFile == true) {
|
||||||
when (chapterPath.extension?.lowercase()) {
|
getPagesFromArchive(chapterPath)
|
||||||
"epub" -> getPagesFromEpub(chapterPath)
|
|
||||||
"txt" -> getPagesFromText(chapterPath)
|
|
||||||
else -> getPagesFromArchive(chapterPath)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
getPagesFromDirectory()
|
getPagesFromDirectory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findDownloadedDocument(): UniFile? {
|
||||||
|
val dbChapter = chapter.chapter
|
||||||
|
return downloadProvider.findChapterDocument(
|
||||||
|
dbChapter.name,
|
||||||
|
dbChapter.scanlator,
|
||||||
|
dbChapter.url,
|
||||||
|
manga.title,
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirror SAF-backed documents into the app cache before opening them.
|
||||||
|
* EPUB uses mmap internally, which is not guaranteed to work with every
|
||||||
|
* document provider even though regular stream reads/writes succeed.
|
||||||
|
*/
|
||||||
|
private fun prepareDownloadedDocument(file: UniFile): UniFile {
|
||||||
|
val extension = file.extension?.lowercase()
|
||||||
|
?: error("Downloaded document has no extension: ${file.name}")
|
||||||
|
val sourceLength = file.length()
|
||||||
|
if (sourceLength <= 0L) {
|
||||||
|
error("Downloaded document is empty: ${file.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val cacheDir = File(context.cacheDir, "downloaded_reader").apply { mkdirs() }
|
||||||
|
val prefix = "${source.id}-${chapter.chapter.id}-"
|
||||||
|
val target = File(
|
||||||
|
cacheDir,
|
||||||
|
"$prefix$sourceLength-${file.lastModified()}.$extension",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!target.isFile || target.length() != sourceLength) {
|
||||||
|
cacheDir.listFiles()
|
||||||
|
?.filter { it.name.startsWith(prefix) }
|
||||||
|
?.forEach { it.delete() }
|
||||||
|
|
||||||
|
val temp = File(cacheDir, "${target.name}.part")
|
||||||
|
if (temp.exists()) temp.delete()
|
||||||
|
file.openInputStream().use { input ->
|
||||||
|
temp.outputStream().use { output -> input.copyTo(output) }
|
||||||
|
}
|
||||||
|
if (temp.length() != sourceLength) {
|
||||||
|
temp.delete()
|
||||||
|
error("Downloaded document cache copy is incomplete: ${file.name}")
|
||||||
|
}
|
||||||
|
if (!temp.renameTo(target)) {
|
||||||
|
temp.copyTo(target, overwrite = true)
|
||||||
|
temp.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UniFile.fromFile(target)
|
||||||
|
?: error("Unable to open cached downloaded document: ${target.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getFallbackPages(): List<ReaderPage> {
|
||||||
|
pageLoader?.recycle()
|
||||||
|
val fallback = documentFallback?.invoke()
|
||||||
|
?: error("Downloaded document fallback is unavailable")
|
||||||
|
pageLoader = fallback
|
||||||
|
val pages = fallback.getPages()
|
||||||
|
isLocal = fallback.isLocal
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
override fun recycle() {
|
override fun recycle() {
|
||||||
super.recycle()
|
super.recycle()
|
||||||
pageLoader?.recycle()
|
pageLoader?.recycle()
|
||||||
|
|||||||
Reference in New Issue
Block a user