mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 03:21:51 +01:00
Basic epub support
This commit is contained in:
parent
08f2cd2472
commit
aeef8c02d8
@ -8,6 +8,8 @@ import eu.kanade.tachiyomi.util.ChapterRecognition
|
|||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.ZipContentProvider
|
import eu.kanade.tachiyomi.util.ZipContentProvider
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -57,64 +59,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override fun toString() = context.getString(R.string.local_source)
|
override fun toString() = context.getString(R.string.local_source)
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga) = Observable.just(manga)
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
|
||||||
val chapters = getBaseDirectories(context)
|
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
|
||||||
.flatten()
|
|
||||||
.filter { it.isDirectory || isSupportedFormat(it.extension) }
|
|
||||||
.map { chapterFile ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = "${manga.url}/${chapterFile.name}"
|
|
||||||
val chapName = if (chapterFile.isDirectory) {
|
|
||||||
chapterFile.name
|
|
||||||
} else {
|
|
||||||
chapterFile.nameWithoutExtension
|
|
||||||
}
|
|
||||||
val chapNameCut = chapName.replace(manga.title, "", true).trim()
|
|
||||||
name = if (chapNameCut.isEmpty()) chapName else chapNameCut
|
|
||||||
date_upload = chapterFile.lastModified()
|
|
||||||
ChapterRecognition.parseChapterNumber(this, manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sortedWith(Comparator<SChapter> { c1, c2 ->
|
|
||||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
|
||||||
if (c == 0) comparator.compare(c2.name, c1.name) else c
|
|
||||||
})
|
|
||||||
|
|
||||||
return Observable.just(chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
val baseDirs = getBaseDirectories(context)
|
|
||||||
|
|
||||||
for (dir in baseDirs) {
|
|
||||||
val chapFile = File(dir, chapter.url)
|
|
||||||
if (!chapFile.exists()) continue
|
|
||||||
|
|
||||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
|
||||||
|
|
||||||
val pageList = if (chapFile.isDirectory) {
|
|
||||||
chapFile.listFiles()
|
|
||||||
.filter { !it.isDirectory && DiskUtil.isImage(it.name, { FileInputStream(it) }) }
|
|
||||||
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
|
||||||
.map { Uri.fromFile(it) }
|
|
||||||
} else {
|
|
||||||
val zip = ZipFile(chapFile)
|
|
||||||
zip.entries().toList()
|
|
||||||
.filter { !it.isDirectory && DiskUtil.isImage(it.name, { zip.getInputStream(it) }) }
|
|
||||||
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
|
||||||
.map { Uri.parse("content://${ZipContentProvider.PROVIDER}${chapFile.absolutePath}!/${it.name}") }
|
|
||||||
}.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
|
||||||
|
|
||||||
return Observable.just(pageList)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Observable.error(Exception("Chapter not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
@ -181,11 +125,174 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga) = Observable.just(manga)
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||||
|
val chapters = getBaseDirectories(context)
|
||||||
|
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||||
|
.flatten()
|
||||||
|
.filter { it.isDirectory || isSupportedFormat(it.extension) }
|
||||||
|
.map { chapterFile ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = "${manga.url}/${chapterFile.name}"
|
||||||
|
val chapName = if (chapterFile.isDirectory) {
|
||||||
|
chapterFile.name
|
||||||
|
} else {
|
||||||
|
chapterFile.nameWithoutExtension
|
||||||
|
}
|
||||||
|
val chapNameCut = chapName.replace(manga.title, "", true).trim()
|
||||||
|
name = if (chapNameCut.isEmpty()) chapName else chapNameCut
|
||||||
|
date_upload = chapterFile.lastModified()
|
||||||
|
ChapterRecognition.parseChapterNumber(this, manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortedWith(Comparator<SChapter> { c1, c2 ->
|
||||||
|
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||||
|
if (c == 0) comparator.compare(c2.name, c1.name) else c
|
||||||
|
})
|
||||||
|
|
||||||
|
return Observable.just(chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
val baseDirs = getBaseDirectories(context)
|
||||||
|
|
||||||
|
for (dir in baseDirs) {
|
||||||
|
val chapFile = File(dir, chapter.url)
|
||||||
|
if (!chapFile.exists()) continue
|
||||||
|
|
||||||
|
return Observable.just(getLoader(chapFile).load())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.error(Exception("Chapter not found"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun isSupportedFormat(extension: String): Boolean {
|
private fun isSupportedFormat(extension: String): Boolean {
|
||||||
return extension.equals("zip", true) || extension.equals("cbz", true)
|
return extension.equals("zip", true) || extension.equals("cbz", true) || extension.equals("epub", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLoader(file: File): Loader {
|
||||||
|
val extension = file.extension
|
||||||
|
return if (file.isDirectory) {
|
||||||
|
DirectoryLoader(file)
|
||||||
|
} else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
||||||
|
ZipLoader(file)
|
||||||
|
} else if (extension.equals("epub", true)) {
|
||||||
|
EpubLoader(file)
|
||||||
|
} else {
|
||||||
|
throw Exception("Invalid chapter format")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Filter.Sort.Selection(0, true))
|
private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Filter.Sort.Selection(0, true))
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(OrderBy())
|
override fun getFilterList() = FilterList(OrderBy())
|
||||||
|
|
||||||
|
interface Loader {
|
||||||
|
fun load(): List<Page>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectoryLoader(val file: File) : Loader {
|
||||||
|
override fun load(): List<Page> {
|
||||||
|
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||||
|
return file.listFiles()
|
||||||
|
.filter { !it.isDirectory && DiskUtil.isImage(it.name, { FileInputStream(it) }) }
|
||||||
|
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
||||||
|
.map { Uri.fromFile(it) }
|
||||||
|
.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZipLoader(val file: File) : Loader {
|
||||||
|
override fun load(): List<Page> {
|
||||||
|
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||||
|
ZipFile(file).use { zip ->
|
||||||
|
return zip.entries().toList()
|
||||||
|
.filter { !it.isDirectory && DiskUtil.isImage(it.name, { zip.getInputStream(it) }) }
|
||||||
|
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
||||||
|
.map { Uri.parse("content://${ZipContentProvider.PROVIDER}${file.absolutePath}!/${it.name}") }
|
||||||
|
.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EpubLoader(val file: File) : Loader {
|
||||||
|
|
||||||
|
override fun load(): List<Page> {
|
||||||
|
ZipFile(file).use { zip ->
|
||||||
|
val allEntries = zip.entries().toList()
|
||||||
|
val ref = getPackageHref(zip)
|
||||||
|
val doc = getPackageDocument(zip, ref)
|
||||||
|
val pages = getPagesFromDocument(doc)
|
||||||
|
val hrefs = getHrefMap(ref, allEntries.map { it.name })
|
||||||
|
return getImagesFromPages(zip, pages, hrefs)
|
||||||
|
.map { Uri.parse("content://${ZipContentProvider.PROVIDER}${file.absolutePath}!/$it") }
|
||||||
|
.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the package document.
|
||||||
|
*/
|
||||||
|
private fun getPackageHref(zip: ZipFile): String {
|
||||||
|
val meta = zip.getEntry("META-INF/container.xml")
|
||||||
|
if (meta != null) {
|
||||||
|
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
|
||||||
|
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
|
||||||
|
if (path != null) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "OEBPS/content.opf"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the package document where all the files are listed.
|
||||||
|
*/
|
||||||
|
private fun getPackageDocument(zip: ZipFile, ref: String): Document {
|
||||||
|
val entry = zip.getEntry(ref)
|
||||||
|
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the pages from the epub.
|
||||||
|
*/
|
||||||
|
private fun getPagesFromDocument(document: Document): List<String> {
|
||||||
|
val pages = document.select("manifest > item")
|
||||||
|
.filter { "application/xhtml+xml" == it.attr("media-type") }
|
||||||
|
.associateBy { it.attr("id") }
|
||||||
|
|
||||||
|
val spine = document.select("spine > itemref").map { it.attr("idref") }
|
||||||
|
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the images contained in every page from the epub.
|
||||||
|
*/
|
||||||
|
private fun getImagesFromPages(zip: ZipFile, pages: List<String>, hrefs: Map<String, String>): List<String> {
|
||||||
|
return pages.map { page ->
|
||||||
|
val entry = zip.getEntry(hrefs[page])
|
||||||
|
val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||||
|
document.getElementsByTag("img").mapNotNull { hrefs[it.attr("src")] }
|
||||||
|
}.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a map with a relative url as key and abolute url as path.
|
||||||
|
*/
|
||||||
|
private fun getHrefMap(packageHref: String, entries: List<String>): Map<String, String> {
|
||||||
|
val lastSlashPos = packageHref.lastIndexOf('/')
|
||||||
|
if (lastSlashPos < 0) {
|
||||||
|
return entries.associateBy { it }
|
||||||
|
}
|
||||||
|
return entries.associateBy { entry ->
|
||||||
|
if (entry.isNotBlank() && entry.length > lastSlashPos) {
|
||||||
|
entry.substring(lastSlashPos + 1)
|
||||||
|
} else {
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user