merge(fix): 227-url-decoding into develop

This commit is contained in:
Acclorite 2025-06-09 15:43:59 +03:00
commit 19db53473d
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
5 changed files with 50 additions and 31 deletions

View file

@ -16,6 +16,8 @@ import ua.acclorite.book_story.core.helpers.clearMarkdown
import ua.acclorite.book_story.core.helpers.containsVisibleText import ua.acclorite.book_story.core.helpers.containsVisibleText
import ua.acclorite.book_story.domain.model.reader.ReaderText import ua.acclorite.book_story.domain.model.reader.ReaderText
import java.io.File import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject
@ -71,7 +73,7 @@ class DocumentParser @Inject constructor(
if (!link.startsWith("http") || element.wholeText().isBlank()) return@forEach if (!link.startsWith("http") || element.wholeText().isBlank()) return@forEach
if (link.startsWith("http://")) { if (link.startsWith("http://")) {
link = link.replace("http://", "https://") link = link.replaceFirst("http://", "https://")
} }
element.prepend("[") element.prepend("[")
@ -84,6 +86,7 @@ class DocumentParser @Inject constructor(
.trim() .trim()
.substringAfterLast(File.separator) .substringAfterLast(File.separator)
.lowercase() .lowercase()
.let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
.takeIf { .takeIf {
it.containsVisibleText() && imageEntries?.any { image -> it.containsVisibleText() && imageEntries?.any { image ->
it == image.name.substringAfterLast(File.separator).lowercase() it == image.name.substringAfterLast(File.separator).lowercase()
@ -103,6 +106,7 @@ class DocumentParser @Inject constructor(
.trim() .trim()
.substringAfterLast(File.separator) .substringAfterLast(File.separator)
.lowercase() .lowercase()
.let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
.takeIf { .takeIf {
it.containsVisibleText() && imageEntries?.any { image -> it.containsVisibleText() && imageEntries?.any { image ->
it == image.name.substringAfterLast(File.separator).lowercase() it == image.name.substringAfterLast(File.separator).lowercase()

View file

@ -11,6 +11,5 @@ import ua.acclorite.book_story.data.model.library.BookWithCover
interface FileParser { interface FileParser {
suspend fun parse(cachedFile: CachedFile): BookWithCover? suspend fun parse(cachedFile: CachedFile): BookWithCover?
} }

View file

@ -10,6 +10,5 @@ import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.domain.model.reader.ReaderText import ua.acclorite.book_story.domain.model.reader.ReaderText
interface TextParser { interface TextParser {
suspend fun parse(cachedFile: CachedFile): List<ReaderText> suspend fun parse(cachedFile: CachedFile): List<ReaderText>
} }

View file

@ -11,6 +11,7 @@ import android.graphics.BitmapFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.parser.Parser
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.model.file.CachedFile import ua.acclorite.book_story.data.model.file.CachedFile
@ -18,6 +19,8 @@ import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.library.Book import ua.acclorite.book_story.domain.model.library.Book
import java.io.File import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject
@ -40,7 +43,7 @@ class EpubFileParser @Inject constructor() : FileParser {
.getInputStream(opfEntry) .getInputStream(opfEntry)
.bufferedReader() .bufferedReader()
.use { it.readText() } .use { it.readText() }
val document = Jsoup.parse(opfContent) val document = Jsoup.parse(opfContent, Parser.xmlParser())
val title = document.select("metadata > dc|title").text().trim().run { val title = document.select("metadata > dc|title").text().trim().run {
ifBlank { ifBlank {
@ -67,18 +70,24 @@ class EpubFileParser @Inject constructor() : FileParser {
val coverImage = document val coverImage = document
.select("metadata > meta[name=cover]") .select("metadata > meta[name=cover]")
.attr("content") .attr("content")
.run { .let { coverId ->
if (isNotBlank()) { if (coverId.isNotBlank()) {
document document
.select("manifest > item[id=$this]") .select("manifest > item[id=$coverId]")
.attr("href") .attr("href")
.apply { if (isNotBlank()) return@run this } .also { src ->
if (src.isBlank()) return@also
return@let src
}
} }
document document
.select("manifest > item[media-type*=image]") .select("manifest > item[media-type*=image]")
.firstOrNull()?.attr("href") .firstOrNull()
?.attr("href")
?.also { src -> if (src.isBlank()) return@let null }
} }
?.let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
book = BookWithCover( book = BookWithCover(
book = Book( book = Book(

View file

@ -17,6 +17,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.parser.Parser
import ua.acclorite.book_story.core.data.ExtensionsData import ua.acclorite.book_story.core.data.ExtensionsData
import ua.acclorite.book_story.core.helpers.addAll import ua.acclorite.book_story.core.helpers.addAll
import ua.acclorite.book_story.core.helpers.containsVisibleText import ua.acclorite.book_story.core.helpers.containsVisibleText
@ -25,6 +26,8 @@ import ua.acclorite.book_story.data.parser.DocumentParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.model.reader.ReaderText import ua.acclorite.book_story.domain.model.reader.ReaderText
import java.io.File import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -117,7 +120,7 @@ class EpubTextParser @Inject constructor(
val unformattedText = ConcurrentLinkedQueue<Pair<Int, List<ReaderText>>>() val unformattedText = ConcurrentLinkedQueue<Pair<Int, List<ReaderText>>>()
// Asynchronously getting all chapters with text // Asynchronously getting all chapters with text
val jobs = chapterEntries.mapIndexed { index, entry -> chapterEntries.mapIndexed { index, entry ->
async(dispatcher) { async(dispatcher) {
yield() yield()
@ -131,8 +134,7 @@ class EpubTextParser @Inject constructor(
yield() yield()
} }
} }.awaitAll()
jobs.awaitAll()
// Sorting chapters in correct order // Sorting chapters in correct order
readerText.addAll { readerText.addAll {
@ -167,7 +169,7 @@ class EpubTextParser @Inject constructor(
zip.getInputStream(entry) zip.getInputStream(entry)
}.bufferedReader().use { it.readText() } }.bufferedReader().use { it.readText() }
var readerText = documentParser.parseDocument( var readerText = documentParser.parseDocument(
document = Jsoup.parse(content), document = Jsoup.parse(content, Parser.htmlParser()),
zipFile = zip, zipFile = zip,
imageEntries = imageEntries, imageEntries = imageEntries,
includeChapter = false includeChapter = false
@ -236,21 +238,21 @@ class EpubTextParser @Inject constructor(
} }
val source = navPoint.selectFirst("content")?.attr("src")?.trim() val source = navPoint.selectFirst("content")?.attr("src")?.trim()
.let { source -> .also { src -> if (src.isNullOrBlank()) return@forEach }
if (source.isNullOrBlank()) return@forEach .let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
source.toUri().path ?: source .let { src -> src.toUri().path ?: src }
}.substringAfterLast(File.separator) .substringAfterLast(File.separator)
val parent = navPoint.parent() val parent = navPoint.parent()
.let { parent -> ?.let { parent ->
if (parent == null) return@let null
if (!parent.tagName().equals("navPoint", ignoreCase = true)) return@let null if (!parent.tagName().equals("navPoint", ignoreCase = true)) return@let null
val parentSource = parent.selectFirst("content")?.attr("src")?.trim() val parentSource = parent.selectFirst("content")?.attr("src")?.trim()
.let { parentSource -> .also { src -> if (src.isNullOrBlank()) return@forEach }
if (parentSource.isNullOrBlank()) return@forEach .let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
parentSource.toUri().path ?: parentSource .let { src -> src.toUri().path ?: src }
}.substringAfterLast(File.separator) .substringAfterLast(File.separator)
if (parentSource == source) return@let null if (parentSource == source) return@let null
return@let parentSource return@let parentSource
} }
@ -278,9 +280,7 @@ class EpubTextParser @Inject constructor(
chapterTitleMap: Map<Source, ReaderText.Chapter>? chapterTitleMap: Map<Source, ReaderText.Chapter>?
): ReaderText.Chapter? { ): ReaderText.Chapter? {
if (chapterTitleMap.isNullOrEmpty()) return null if (chapterTitleMap.isNullOrEmpty()) return null
return chapterTitleMap.getOrElse(chapterSource.substringAfterLast(File.separator)) { return chapterTitleMap.getOrElse(chapterSource.substringAfterLast(File.separator)) { null }
null
}
} }
/** /**
@ -297,19 +297,27 @@ class EpubTextParser @Inject constructor(
val opfContent = getInputStream(opfEntry).bufferedReader().use { val opfContent = getInputStream(opfEntry).bufferedReader().use {
it.readText() it.readText()
} }
val document = Jsoup.parse(opfContent) val document = Jsoup.parse(opfContent, Parser.xmlParser())
val zipEntries = entries().toList() val zipEntries = entries().toList()
val manifestItems = document.select("manifest > item").associate { val manifestItems = document.select("manifest > item").associate {
it.attr("id") to it.attr("href") it.attr("id").to(
it.attr("href").let { src ->
URLDecoder.decode(src, StandardCharsets.UTF_8.name())
}
)
} }
document.select("spine > itemref").mapNotNull { itemRef -> document.select("spine > itemref").mapNotNull { itemRef ->
val spineId = itemRef.attr("idref") val spineId = itemRef.attr("idref")
val chapterSource = manifestItems[spineId] val chapterSource = manifestItems[spineId]
?.substringAfterLast(File.separator) .let { src ->
?.lowercase() if (src.isNullOrBlank()) return@mapNotNull null
?: return@mapNotNull null URLDecoder.decode(
src.substringAfterLast(File.separator).lowercase(),
StandardCharsets.UTF_8.name()
)
}
zipEntries.find { entry -> zipEntries.find { entry ->
entry.name.substringAfterLast(File.separator).lowercase() == chapterSource entry.name.substringAfterLast(File.separator).lowercase() == chapterSource