fix(parser): decode URLs in HTML
Co-authored-by: attilajam <attila.jamilov@gmail.com> Resolves: #227
This commit is contained in:
parent
798958c56b
commit
aa665b33f8
5 changed files with 50 additions and 31 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue