mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Basic epub support
This commit is contained in:
		| @@ -8,6 +8,8 @@ import eu.kanade.tachiyomi.util.ChapterRecognition | ||||
| import eu.kanade.tachiyomi.util.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.ZipContentProvider | ||||
| import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator | ||||
| import org.jsoup.Jsoup | ||||
| import org.jsoup.nodes.Document | ||||
| import rx.Observable | ||||
| import timber.log.Timber | ||||
| 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 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 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 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 { | ||||
|         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)) | ||||
|  | ||||
|     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 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user