Changes to local source from upstream

Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-04-29 01:00:51 -04:00
parent 0885f436e4
commit e1e8202192
2 changed files with 179 additions and 109 deletions

View File

@ -1,42 +1,34 @@
package eu.kanade.tachiyomi.source
import android.content.Context
import com.google.gson.Gson
import com.github.junrar.Archive
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.system.ImageUtil
import junrar.Archive
import junrar.rarfile.FileHeader
import kotlinx.coroutines.runBlocking
import rx.Observable
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.Locale
import java.util.Scanner
import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource {
companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
@ -47,13 +39,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
val dir = getBaseDirectories(context).asSequence().firstOrNull()
val dir = getBaseDirectories(context).firstOrNull()
if (dir == null) {
input.close()
return null
}
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
if (cover.exists()) cover.delete()
cover.parentFile?.mkdirs()
input.use {
@ -93,22 +84,28 @@ class LocalSource(private val context: Context) : CatalogueSource {
if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
var mangaDirs = baseDirs
.asSequence()
.mapNotNull { it.listFiles()?.toList() }.flatten()
.mapNotNull { it.listFiles()?.toList() }
.flatten()
.filter { it.isDirectory }
.filterNot { it.name.startsWith('.') }
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
when (state?.index) {
0 -> {
if (state.ascending) mangaDirs =
mangaDirs = if (state.ascending) {
mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
else mangaDirs =
} else {
mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
}
}
1 -> {
if (state.ascending) mangaDirs = mangaDirs.sortedBy(File::lastModified)
else mangaDirs = mangaDirs.sortedByDescending(File::lastModified)
mangaDirs = if (state.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
}
}
@ -126,12 +123,20 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
}
val chapters = fetchChapterList(this).toBlocking().first()
if (chapters.isNotEmpty()) {
val chapter = chapters.last()
val format = getFormat(chapter)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(this)
}
}
// Copy the cover from the first chapter found.
if (thumbnail_url == null) {
val chapters = runBlocking { getChapterList(toMangaInfo()).map { it.toSChapter() } }
if (chapters.isNotEmpty()) {
try {
val dest = updateCover(chapters.last(), this)
val dest = updateCover(chapter, this)
thumbnail_url = dest?.absolutePath
} catch (e: Exception) {
Timber.e(e)
@ -140,52 +145,31 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
}
}
return Observable.just(MangasPage(mangas.toList(), false))
}
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
val baseDirs = getBaseDirectories(context)
baseDirs
getBaseDirectories(context)
.asSequence()
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten()
.filter { it.extension == "json" }.firstOrNull()?.apply {
val json = Gson().fromJson(
Scanner(this).useDelimiter("\\Z").next(),
JsonObject::class.java
)
.firstOrNull { it.extension == "json" }
?.apply {
val reader = this.inputStream().bufferedReader()
val json = JsonParser.parseReader(reader).asJsonObject
manga.title = json["title"]?.asString ?: manga.title
manga.author = json["author"]?.asString ?: manga.author
manga.artist = json["artist"]?.asString ?: manga.artist
manga.description = json["description"]?.asString ?: manga.description
manga.genre = json["genre"]?.asJsonArray?.map { it.asString }?.joinToString(", ")
manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
?: manga.genre
manga.status = json["status"]?.asInt ?: manga.status
}
val url = manga.url
// Try to find the cover
for (dir in baseDirs) {
val cover = File("${dir.absolutePath}/$url", COVER_NAME)
if (cover.exists()) {
manga.thumbnail_url = cover.absolutePath
break
}
}
// Copy the cover from the first chapter found.
if (manga.thumbnail_url == null) {
val chapters = runBlocking { getChapterList(manga.toMangaInfo()).map { it.toSChapter() } }
if (chapters.isNotEmpty()) {
try {
val dest = updateCover(chapters.last(), manga)
manga.thumbnail_url = dest?.absolutePath
} catch (e: Exception) {
Timber.e(e)
}
}
}
return Observable.just(manga)
}
@ -229,33 +213,78 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters =
getBaseDirectories(context).mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten().filter { it.isDirectory || isSupportedFile(it.extension) }
val chapters = getBaseDirectories(context)
.asSequence()
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten()
.filter { it.isDirectory || isSupportedFile(it.extension) }
.map { chapterFile ->
SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}"
val chapName = if (chapterFile.isDirectory) {
name = 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()
val format = getFormat(this)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillChapterMetadata(this)
}
}
val chapNameCut = stripMangaTitle(name, manga.title)
if (chapNameCut.isNotEmpty()) name = chapNameCut
ChapterRecognition.parseChapterNumber(this, manga)
}
}.sortedWith(
}
.sortedWith(
Comparator { c1, c2 ->
val c = c2.chapter_number.compareTo(c1.chapter_number)
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
}
)
.toList()
return Observable.just(chapters)
}
/**
* Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
* characters.
*/
private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
var chapterNameIndex = 0
var mangaTitleIndex = 0
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
val chapterChar = chapterName[chapterNameIndex]
val mangaChar = mangaTitle[mangaTitleIndex]
if (!chapterChar.equals(mangaChar, true)) {
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
if (!invalidChapterChar && !invalidMangaChar) {
return chapterName
}
if (invalidChapterChar) {
chapterNameIndex++
}
if (invalidMangaChar) {
mangaTitleIndex++
}
} else {
chapterNameIndex++
mangaTitleIndex++
}
}
return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(Exception("Unused"))
}
@ -292,52 +321,37 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
private fun updateCover(chapter: SChapter, manga: SManga): File? {
val format = getFormat(chapter)
return when (format) {
return when (val format = getFormat(chapter)) {
is Format.Directory -> {
val entry = format.file.listFiles()
?.sortedWith(
Comparator<File> { f1, f2 ->
f1.name.compareToCaseInsensitiveNaturalOrder(f2.name)
}
)
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream()) }
}
is Format.Zip -> {
ZipFile(format.file).use { zip ->
val entry = zip.entries().toList().sortedWith(
Comparator<ZipEntry> { f1, f2 ->
f1.name.compareToCaseInsensitiveNaturalOrder(f2.name)
}
).find {
!it.isDirectory && ImageUtil.isImage(it.name) {
zip.getInputStream(it)
}
}
val entry = zip.entries().toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
}
}
is Format.Rar -> {
Archive(format.file).use { archive ->
val entry = archive.fileHeaders.sortedWith(
Comparator<FileHeader> { f1, f2 ->
f1.fileNameString.compareToCaseInsensitiveNaturalOrder(f2.fileNameString)
}
).find {
!it.isDirectory && ImageUtil.isImage(it.fileNameString) {
archive.getInputStream(it)
}
}
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
}
}
is Format.Epub -> {
EpubFile(format.file).use { epub ->
val entry = epub.getImagesFromPages().firstOrNull()?.let { epub.getEntry(it) }
val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
}
@ -345,8 +359,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
}
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"), Selection(0, true))
override fun getFilterList() = FilterList(OrderBy())

View File

@ -1,10 +1,15 @@
package eu.kanade.tachiyomi.util.storage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@ -44,6 +49,58 @@ class EpubFile(file: File) : Closeable {
return zip.getEntry(name)
}
/**
* Fills manga metadata using this epub file's metadata.
*/
fun fillMangaMetadata(manga: SManga) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val creator = doc.getElementsByTag("dc:creator").first()
val description = doc.getElementsByTag("dc:description").first()
manga.author = creator?.text()
manga.description = description?.text()
}
/**
* Fills chapter metadata using this epub file's metadata.
*/
fun fillChapterMetadata(chapter: SChapter) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val title = doc.getElementsByTag("dc:title").first()
val publisher = doc.getElementsByTag("dc:publisher").first()
val creator = doc.getElementsByTag("dc:creator").first()
var date = doc.getElementsByTag("dc:date").first()
if (date == null) {
date = doc.select("meta[property=dcterms:modified]").first()
}
if (title != null) {
chapter.name = title.text()
}
if (publisher != null) {
chapter.scanlator = publisher.text()
} else if (creator != null) {
chapter.scanlator = creator.text()
}
if (date != null) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
try {
val parsedDate = dateFormat.parse(date.text())
if (parsedDate != null) {
chapter.date_upload = parsedDate.time
}
} catch (e: ParseException) {
// Empty
}
}
}
/**
* Returns the path of all the images found in the epub file.
*/
@ -93,7 +150,7 @@ class EpubFile(file: File) : Closeable {
* Returns all the images contained in every page from the epub.
*/
private fun getImagesFromPages(pages: List<String>, packageHref: String): List<String> {
val result = ArrayList<String>()
val result = mutableListOf<String>()
val basePath = getParentDirectory(packageHref)
pages.forEach { page ->
val entryPath = resolveZipPath(basePath, page)
@ -118,10 +175,10 @@ class EpubFile(file: File) : Closeable {
*/
private fun getPathSeparator(): String {
val meta = zip.getEntry("META-INF\\container.xml")
if (meta != null) {
return "\\"
return if (meta != null) {
"\\"
} else {
return "/"
"/"
}
}
@ -149,10 +206,10 @@ class EpubFile(file: File) : Closeable {
*/
private fun getParentDirectory(path: String): String {
val separatorIndex = path.lastIndexOf(pathSeparator)
if (separatorIndex >= 0) {
return path.substring(0, separatorIndex)
return if (separatorIndex >= 0) {
path.substring(0, separatorIndex)
} else {
return ""
""
}
}
}