🛠️ Improve EPUB parser
* Fixed some issues with ordering * Better toc.ncx parsing * Fixed issues with missing chapters
This commit is contained in:
parent
ff1a43d444
commit
89e2f0a326
1 changed files with 75 additions and 135 deletions
|
|
@ -40,22 +40,13 @@ class EpubTextParser @Inject constructor(
|
||||||
zip.entries().asSequence().find { entry ->
|
zip.entries().asSequence().find { entry ->
|
||||||
entry.name.endsWith("toc.ncx", ignoreCase = true)
|
entry.name.endsWith("toc.ncx", ignoreCase = true)
|
||||||
}.apply {
|
}.apply {
|
||||||
if (this == null) {
|
parseEpub(tocEntry = this, zip = zip).let {
|
||||||
Log.w(EPUB_TAG, "toc.ncx was not found.")
|
if (it == null) {
|
||||||
parseWithoutToc(zip)?.let { chapters.addAll(it) }
|
Log.e(EPUB_TAG, "Could not parse EPUB.")
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
|
chapters.addAll(it)
|
||||||
parseWithToc(this, zip).apply {
|
|
||||||
if (this == null) {
|
|
||||||
parseWithoutToc(zip)?.let { chapters.addAll(it) }
|
|
||||||
return@withContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chapters.addAll(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
yield()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,45 +71,89 @@ class EpubTextParser @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses text if no toc.ncx found, which is Table of Content.
|
* Parses text and chapters from EPUB.
|
||||||
|
* Uses toc.ncx(if present) to retrieve titles, otherwise uses first line as title.
|
||||||
*
|
*
|
||||||
* @return Null if could not parse.
|
* @return Null if could not parse.
|
||||||
*/
|
*/
|
||||||
private suspend fun parseWithoutToc(zip: ZipFile): List<ChapterWithText>? {
|
private suspend fun parseEpub(tocEntry: ZipEntry?, zip: ZipFile): List<ChapterWithText>? {
|
||||||
val chapters = mutableListOf<ChapterWithText>()
|
Log.i(EPUB_TAG, "TOC Entry: ${tocEntry?.name ?: "NO TOC"}")
|
||||||
var chapterTextIndex = -1
|
|
||||||
var chapterIndex = 1
|
|
||||||
|
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
zip.entries().asSequence().sortedBy { it.name }.forEach { entry ->
|
val chapters = mutableListOf<ChapterWithText>()
|
||||||
|
var chapterTextIndex = -1
|
||||||
|
|
||||||
|
val tocContent = tocEntry?.let {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
zip.getInputStream(it)
|
||||||
|
}.bufferedReader().use { it.readText() }
|
||||||
|
}
|
||||||
|
val tocDocument = tocContent?.let { Jsoup.parse(it) }
|
||||||
|
val chaptersTitles = tocDocument.run {
|
||||||
|
if (this == null) return@run null
|
||||||
|
var titles = mutableMapOf<String, List<String>>()
|
||||||
|
|
||||||
|
select("navPoint").forEach { navPoint ->
|
||||||
|
val title = navPoint.selectFirst("navLabel > text")?.text()?.trim()
|
||||||
|
?: return@forEach
|
||||||
|
val source = navPoint.selectFirst("content")?.attr("src")?.trim().let {
|
||||||
|
if (it == null) return@forEach
|
||||||
|
Uri.parse(it).path ?: it
|
||||||
|
}.substringAfterLast("/")
|
||||||
|
|
||||||
|
titles[source] = (titles[source] ?: emptyList()) + title
|
||||||
|
}
|
||||||
|
|
||||||
|
titles
|
||||||
|
}
|
||||||
|
|
||||||
|
yield()
|
||||||
|
|
||||||
|
zip.entries().asSequence().sortedBy {
|
||||||
|
it.name.filter { it.isDigit() }.toIntOrNull()
|
||||||
|
}.forEach { entry ->
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!entry.name.endsWith(".xhtml")
|
!entry.name.endsWith(".xhtml")
|
||||||
&& !entry.name.endsWith(".html")
|
&& !entry.name.endsWith(".html")
|
||||||
&& !entry.name.endsWith(".xml")
|
|
||||||
&& !entry.name.endsWith(".htm")
|
&& !entry.name.endsWith(".htm")
|
||||||
|| entry.name.endsWith("container.xml")
|
|
||||||
) return@forEach
|
) return@forEach
|
||||||
|
|
||||||
val content = zip.getInputStream(entry)
|
yield()
|
||||||
.bufferedReader()
|
|
||||||
.use {
|
|
||||||
it.readText()
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapter =
|
val content = zip.getInputStream(entry).bufferedReader().use { it.readText() }
|
||||||
documentParser.run { Jsoup.parse(content).parseDocument() }.toMutableList()
|
var chapter = documentParser.run {
|
||||||
val chapterTitle = chapter.firstOrNull()?.clearMarkdown()
|
Jsoup.parse(content).parseDocument()
|
||||||
?: "Chapter $chapterIndex" // In case could not get first line(shouldn't happen)
|
}
|
||||||
chapter.removeFirstOrNull()
|
|
||||||
|
|
||||||
if (chapter.isEmpty()) {
|
if (chapter.isEmpty()) {
|
||||||
Log.w(EPUB_TAG, "Chapter ${entry.name} is empty.")
|
Log.w(EPUB_TAG, "Chapter ${entry.name} is empty.")
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chapterTitle = getChapterTitleFromToc(
|
||||||
|
chapterSource = entry.name,
|
||||||
|
chaptersTitles = chaptersTitles
|
||||||
|
).run {
|
||||||
|
if (this != null) {
|
||||||
|
return@run this
|
||||||
|
}
|
||||||
|
chapter.first().clearMarkdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter = chapter.dropWhile {
|
||||||
|
it.clearMarkdown().lowercase() == chapterTitle.lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chapter.isEmpty()) {
|
||||||
|
Log.w(EPUB_TAG, "Chapter ${entry.name} is empty.")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
yield()
|
||||||
|
|
||||||
chapters.add(
|
chapters.add(
|
||||||
ChapterWithText(
|
ChapterWithText(
|
||||||
chapter = Chapter(
|
chapter = Chapter(
|
||||||
|
|
@ -131,7 +166,6 @@ class EpubTextParser @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
chapterTextIndex += chapter.size
|
chapterTextIndex += chapter.size
|
||||||
chapterIndex++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield()
|
yield()
|
||||||
|
|
@ -144,108 +178,14 @@ class EpubTextParser @Inject constructor(
|
||||||
return chapters
|
return chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun getChapterTitleFromToc(
|
||||||
* Parses text with toc.ncx. Extracts all chapters.
|
chapterSource: String,
|
||||||
*
|
chaptersTitles: Map<String, List<String>>?
|
||||||
* @return Null if could not parse toc.ncx.
|
): String? {
|
||||||
*/
|
if (chaptersTitles.isNullOrEmpty()) return null
|
||||||
private suspend fun parseWithToc(tocEntry: ZipEntry, zip: ZipFile): List<ChapterWithText>? {
|
return chaptersTitles
|
||||||
Log.i(EPUB_TAG, "TOC Entry: ${tocEntry.name}")
|
.getOrElse(chapterSource.substringAfterLast("/")) { null }
|
||||||
|
?.joinToString(separator = " / ")
|
||||||
val chapters = mutableMapOf<String, ChapterWithText>()
|
?.ifBlank { null }
|
||||||
var emptyChapters = 0
|
|
||||||
var chapterTextIndex = -1
|
|
||||||
var chapterIndex = 1
|
|
||||||
|
|
||||||
yield()
|
|
||||||
|
|
||||||
val tocContent = withContext(Dispatchers.IO) {
|
|
||||||
zip.getInputStream(tocEntry)
|
|
||||||
}.bufferedReader().use { it.readText() }
|
|
||||||
val tocDocument = Jsoup.parse(tocContent)
|
|
||||||
|
|
||||||
yield()
|
|
||||||
|
|
||||||
tocDocument.select("navPoint").forEach { navPoint ->
|
|
||||||
yield()
|
|
||||||
|
|
||||||
val chapterTitle = navPoint.selectFirst("navLabel > text")?.text()?.trim()
|
|
||||||
?: "Chapter $chapterIndex"
|
|
||||||
val chapterSrc = navPoint.selectFirst("content")?.attr("src")?.trim()
|
|
||||||
.run {
|
|
||||||
if (this == null) {
|
|
||||||
Log.e(EPUB_TAG, "No source of the chapter found: $chapterTitle")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri.parse(this).path ?: this
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chapters.containsKey(chapterSrc)) {
|
|
||||||
chapters[chapterSrc] = chapters[chapterSrc]!!.run {
|
|
||||||
copy(
|
|
||||||
chapter = chapter.copy(
|
|
||||||
title = "${chapter.title} / $chapterTitle"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
zip.entries().asSequence().find { entry ->
|
|
||||||
entry.name.endsWith(chapterSrc)
|
|
||||||
}.apply {
|
|
||||||
if (this == null) {
|
|
||||||
Log.e(EPUB_TAG, "No chapter entry found: $chapterTitle")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val content = zip.getInputStream(this)
|
|
||||||
.bufferedReader()
|
|
||||||
.use {
|
|
||||||
it.readText()
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapter = documentParser.run {
|
|
||||||
Jsoup.parse(content).parseDocument().dropWhile {
|
|
||||||
it.clearMarkdown().lowercase() == chapterTitle.lowercase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (chapter.isEmpty()) {
|
|
||||||
Log.w(EPUB_TAG, "Chapter $chapterTitle is empty.")
|
|
||||||
emptyChapters += 1
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
chapters.put(
|
|
||||||
key = chapterSrc,
|
|
||||||
value = ChapterWithText(
|
|
||||||
chapter = Chapter(
|
|
||||||
index = chapters.size,
|
|
||||||
title = chapterTitle,
|
|
||||||
startIndex = chapterTextIndex + 1,
|
|
||||||
endIndex = chapterTextIndex + chapter.size
|
|
||||||
),
|
|
||||||
text = chapter
|
|
||||||
)
|
|
||||||
)
|
|
||||||
chapterTextIndex += chapter.size
|
|
||||||
chapterIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yield()
|
|
||||||
|
|
||||||
if (chapters.isEmpty()) {
|
|
||||||
Log.e(EPUB_TAG, "Could not parse text with toc.ncx")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptyChapters >= ((emptyChapters + chapters.size) * 0.25f)) {
|
|
||||||
Log.e(EPUB_TAG, "More than 25% of the chapters are empty.")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapters.values.toList().sortedBy { it.chapter.index }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue