fix(parser): decode URLs in HTML

Co-authored-by: attilajam <attila.jamilov@gmail.com>
Resolves: #227
This commit is contained in:
Acclorite 2025-06-09 15:42:37 +03:00
parent 798958c56b
commit aa665b33f8
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.domain.model.reader.ReaderText
import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
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://")) {
link = link.replace("http://", "https://")
link = link.replaceFirst("http://", "https://")
}
element.prepend("[")
@ -84,6 +86,7 @@ class DocumentParser @Inject constructor(
.trim()
.substringAfterLast(File.separator)
.lowercase()
.let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
.takeIf {
it.containsVisibleText() && imageEntries?.any { image ->
it == image.name.substringAfterLast(File.separator).lowercase()
@ -103,6 +106,7 @@ class DocumentParser @Inject constructor(
.trim()
.substringAfterLast(File.separator)
.lowercase()
.let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
.takeIf {
it.containsVisibleText() && imageEntries?.any { image ->
it == image.name.substringAfterLast(File.separator).lowercase()

View file

@ -11,6 +11,5 @@ import ua.acclorite.book_story.data.model.library.BookWithCover
interface FileParser {
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
interface TextParser {
suspend fun parse(cachedFile: CachedFile): List<ReaderText>
}

View file

@ -11,6 +11,7 @@ import android.graphics.BitmapFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.jsoup.parser.Parser
import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText
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.domain.model.library.Book
import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile
import javax.inject.Inject
@ -40,7 +43,7 @@ class EpubFileParser @Inject constructor() : FileParser {
.getInputStream(opfEntry)
.bufferedReader()
.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 {
ifBlank {
@ -67,18 +70,24 @@ class EpubFileParser @Inject constructor() : FileParser {
val coverImage = document
.select("metadata > meta[name=cover]")
.attr("content")
.run {
if (isNotBlank()) {
.let { coverId ->
if (coverId.isNotBlank()) {
document
.select("manifest > item[id=$this]")
.select("manifest > item[id=$coverId]")
.attr("href")
.apply { if (isNotBlank()) return@run this }
.also { src ->
if (src.isBlank()) return@also
return@let src
}
}
document
.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 = Book(

View file

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