🚀 Improve EPUB parser
Replace EpubLib parser with custom that uses ZipFile. The previous parser had multiple problems with reading and parsing books. This should resolve most errors, improve compatibility and provide better user experience. Resolves: #42
This commit is contained in:
parent
01173a2b72
commit
3b69b89819
3 changed files with 103 additions and 98 deletions
|
|
@ -1,16 +1,17 @@
|
|||
package ua.acclorite.book_story.data.parser.epub
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.siegmann.epublib.epub.EpubReader
|
||||
import org.jsoup.Jsoup
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
|
||||
class EpubFileParser @Inject constructor() : FileParser {
|
||||
|
|
@ -21,45 +22,83 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
}
|
||||
|
||||
try {
|
||||
val epubReader = EpubReader()
|
||||
val book = withContext(Dispatchers.IO) {
|
||||
FileInputStream(file).use {
|
||||
epubReader.readEpub(it)
|
||||
var book: Pair<Book, CoverImage?>? = null
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
ZipFile(file).use { zip ->
|
||||
val opfEntry = zip.entries().asSequence().find { entry ->
|
||||
entry.name.endsWith(".opf")
|
||||
} ?: return@withContext
|
||||
|
||||
val opfContent = zip
|
||||
.getInputStream(opfEntry)
|
||||
.bufferedReader()
|
||||
.use { it.readText() }
|
||||
val document = Jsoup.parse(opfContent)
|
||||
|
||||
val title = document.select("metadata > dc|title").text().trim()
|
||||
val author = UIText.StringValue(
|
||||
document.select("metadata > dc|creator").text().trim()
|
||||
)
|
||||
val description = Jsoup.parse(
|
||||
document.select("metadata > dc|description").text()
|
||||
).text()
|
||||
|
||||
var coverImagePath: String? = null
|
||||
|
||||
val coverId = document
|
||||
.select("metadata > meta[name=cover]")
|
||||
.attr("content")
|
||||
if (coverId.isNotBlank()) {
|
||||
coverImagePath = document
|
||||
.select("manifest > item[id=$coverId]")
|
||||
.attr("href")
|
||||
}
|
||||
|
||||
if (coverImagePath.isNullOrBlank()) {
|
||||
coverImagePath = document
|
||||
.select("manifest > item[media-type*=image]")
|
||||
.firstOrNull()?.attr("href")
|
||||
}
|
||||
|
||||
if (coverImagePath == null) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val coverImage = extractCoverImageBitmap(file, coverImagePath)
|
||||
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = description,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
) to coverImage
|
||||
}
|
||||
}
|
||||
|
||||
val metadata = book.metadata
|
||||
val author = UIText.StringValue(metadata.authors.joinToString(", "))
|
||||
val title = metadata.titles.joinToString(", ")
|
||||
val coverImage = book.coverImage?.let { BitmapFactory.decodeStream(it.inputStream) }
|
||||
|
||||
var description: StringBuilder? = StringBuilder()
|
||||
metadata.descriptions.forEach {
|
||||
description?.append(it)?.append(" ")
|
||||
}
|
||||
|
||||
if (description != null) {
|
||||
if (description.isBlank()) {
|
||||
description = null
|
||||
}
|
||||
}
|
||||
|
||||
return Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = description?.toString(),
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
) to coverImage
|
||||
return book
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractCoverImageBitmap(file: File, coverImagePath: String): Bitmap? {
|
||||
ZipFile(file).use { zip ->
|
||||
zip.entries().asSequence().forEach { entry ->
|
||||
if (entry.name.endsWith(coverImagePath)) {
|
||||
val imageBytes = zip.getInputStream(entry).readBytes()
|
||||
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -2,19 +2,13 @@ package ua.acclorite.book_story.data.parser.epub
|
|||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.siegmann.epublib.epub.EpubReader
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document.OutputSettings
|
||||
import org.jsoup.safety.Safelist
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.Charset
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
|
@ -26,66 +20,41 @@ class EpubTextParser @Inject constructor() : TextParser {
|
|||
}
|
||||
|
||||
try {
|
||||
val book = withContext(Dispatchers.IO) {
|
||||
EpubReader().readEpub(FileInputStream(file))
|
||||
}
|
||||
val unformattedText = StringBuilder()
|
||||
val lines = mutableListOf<String>()
|
||||
|
||||
for (spineReference in book.spine.spineReferences) {
|
||||
val resource = spineReference.resource
|
||||
val inputStream = resource.inputStream
|
||||
val reader = BufferedReader(
|
||||
InputStreamReader(
|
||||
inputStream,
|
||||
Charset.forName("UTF-8")
|
||||
)
|
||||
)
|
||||
var line: String?
|
||||
withContext(Dispatchers.IO) {
|
||||
ZipFile(file).use { zip ->
|
||||
zip.entries().asSequence().forEach { entry ->
|
||||
if (
|
||||
entry.name.endsWith(".xhtml")
|
||||
|| entry.name.endsWith(".html")
|
||||
|| entry.name.endsWith(".xml")
|
||||
|| entry.name.endsWith(".htm")
|
||||
) {
|
||||
val content = zip.getInputStream(entry).bufferedReader()
|
||||
.use {
|
||||
it.readText()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
inputStream.close()
|
||||
}
|
||||
|
||||
while (withContext(Dispatchers.IO) { reader.readLine() }
|
||||
.also { line = it } != null) {
|
||||
unformattedText.append(line).append("\n")
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
reader.close()
|
||||
}
|
||||
}
|
||||
|
||||
val strings = mutableListOf<String>()
|
||||
|
||||
val parsedText = Jsoup.parse(unformattedText.toString())
|
||||
parsedText.outputSettings(OutputSettings().prettyPrint(false))
|
||||
parsedText.select("br").append("\n")
|
||||
parsedText.select("p").prepend("\n")
|
||||
parsedText.select("em").append(" ").prepend("")
|
||||
|
||||
val formattedText = Jsoup.clean(
|
||||
parsedText.html(),
|
||||
"",
|
||||
Safelist.none(),
|
||||
OutputSettings().prettyPrint(false)
|
||||
)
|
||||
|
||||
formattedText
|
||||
.replace(" ", "")
|
||||
.replace("\u00a0", "")
|
||||
.split("\n")
|
||||
.forEach {
|
||||
if (it.isNotBlank()) {
|
||||
strings.add(it.trim())
|
||||
val document = Jsoup.parse(content)
|
||||
document
|
||||
.wholeText()
|
||||
.lines()
|
||||
.forEach { element ->
|
||||
if (element.isNotBlank()) {
|
||||
lines.add(element.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strings.isEmpty()) {
|
||||
if (lines.isEmpty()) {
|
||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||
}
|
||||
|
||||
return Resource.Success(strings)
|
||||
return Resource.Success(lines)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue