Read metadata from ComicInfo.xml files in Local source (#8025)

Co-authored-by: Shamicen <84282253+Shamicen@users.noreply.github.com>
Co-authored-by: Andreas <andreas.everos@gmail.com>
Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
This commit is contained in:
arkon 2022-09-18 10:55:30 -04:00 committed by GitHub
parent 30b3b2d3ff
commit 1395343f11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 200 additions and 17 deletions

View file

@ -0,0 +1,60 @@
package eu.kanade.domain.manga.model
import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import nl.adaptivity.xmlutil.serialization.XmlValue
@Serializable
@XmlSerialName("ComicInfo", "", "")
data class ComicInfo(
val series: ComicInfoSeries?,
val summary: ComicInfoSummary?,
val writer: ComicInfoWriter?,
val penciller: ComicInfoPenciller?,
val inker: ComicInfoInker?,
val colorist: ComicInfoColorist?,
val letterer: ComicInfoLetterer?,
val coverArtist: ComicInfoCoverArtist?,
val genre: ComicInfoGenre?,
val tags: ComicInfoTags?,
)
@Serializable
@XmlSerialName("Series", "", "")
data class ComicInfoSeries(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Summary", "", "")
data class ComicInfoSummary(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Writer", "", "")
data class ComicInfoWriter(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Penciller", "", "")
data class ComicInfoPenciller(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Inker", "", "")
data class ComicInfoInker(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Colorist", "", "")
data class ComicInfoColorist(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Letterer", "", "")
data class ComicInfoLetterer(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("CoverArtist", "", "")
data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Genre", "", "")
data class ComicInfoGenre(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Tags", "", "")
data class ComicInfoTags(@XmlValue(true) val value: String = "")

View file

@ -30,6 +30,8 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isDevFlavor
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton import uy.kohesive.injekt.api.addSingleton
@ -89,6 +91,13 @@ class AppModule(val app: Application) : InjektModule {
} }
} }
addSingletonFactory {
XML {
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
autoPolymorphic = true
}
}
addSingletonFactory { ChapterCache(app) } addSingletonFactory { ChapterCache(app) }
addSingletonFactory { CoverCache(app) } addSingletonFactory { CoverCache(app) }

View file

@ -168,7 +168,7 @@ object Migrations {
} }
} }
if (oldVersion < 60) { if (oldVersion < 60) {
// Re-enable update check that was prevously accidentally disabled for M // Re-enable update check that was previously accidentally disabled for M
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
AppUpdateJob.setupTask(context) AppUpdateJob.setupTask(context)
} }

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import com.github.junrar.Archive import com.github.junrar.Archive
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.domain.manga.model.ComicInfo
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
@ -20,11 +22,14 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import logcat.LogPriority import logcat.LogPriority
import nl.adaptivity.xmlutil.AndroidXmlReader
import nl.adaptivity.xmlutil.serialization.XML
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -33,6 +38,7 @@ class LocalSource(
) : CatalogueSource, UnmeteredSource { ) : CatalogueSource, UnmeteredSource {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val xml: XML by injectLazy()
override val name: String = context.getString(R.string.local_source) override val name: String = context.getString(R.string.local_source)
@ -134,27 +140,132 @@ class LocalSource(
} }
// Manga details related // Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga { override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
val baseDirsFile = getBaseDirectoriesFiles(context) val baseDirsFile = getBaseDirectoriesFiles(context)
getCoverFile(manga.url, baseDirsFile)?.let { getCoverFile(manga.url, baseDirsFile)?.let {
manga.thumbnail_url = it.absolutePath manga.thumbnail_url = it.absolutePath
} }
getMangaDirsFiles(manga.url, baseDirsFile) // Augment manga details based on metadata files
.firstOrNull { it.extension.equals("json", ignoreCase = true) } try {
?.let { file -> val mangaDirFiles = getMangaDirsFiles(manga.url, baseDirsFile).toList()
json.decodeFromStream<MangaDetails>(file.inputStream()).run { val comicInfoMetadata = mangaDirFiles
title?.let { manga.title = it } .firstOrNull { it.name == COMIC_INFO_FILE || it.name == ".noxml" }
author?.let { manga.author = it }
artist?.let { manga.artist = it } when {
description?.let { manga.description = it } // Top level ComicInfo.xml
genre?.let { manga.genre = it.joinToString() } comicInfoMetadata?.name == COMIC_INFO_FILE -> {
status?.let { manga.status = it } setMangaDetailsFromComicInfoFile(comicInfoMetadata.inputStream(), manga)
}
// Copy ComicInfo.xml from chapter archive to top level if found
comicInfoMetadata == null -> {
val chapterArchives = mangaDirFiles
.filter { isSupportedArchiveFile(it.extension) }
.toList()
val mangaDir = getMangaDir(manga.url, baseDirsFile)
val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
} else {
// Avoid re-scanning
File("$folderPath/.noxml").createNewFile()
}
}
// Fall back to legacy JSON details format
else -> {
mangaDirFiles
.firstOrNull { it.extension == "json" }
?.let { file ->
json.decodeFromStream<MangaDetails>(file.inputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
description?.let { manga.description = it }
genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it }
}
}
} }
} }
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Error setting manga details from local metadata for ${manga.title}" }
}
return manga return@withIOContext manga
}
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
for (chapter in chapterArchives) {
when (getFormat(chapter)) {
is Format.Zip -> {
ZipFile(chapter).use { zip: ZipFile ->
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
zip.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
}
}
}
}
is Format.Rar -> {
Archive(chapter).use { rar: Archive ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
}
}
}
}
else -> {}
}
}
return null
}
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File {
return File("$folderPath/$COMIC_INFO_FILE").apply {
outputStream().use { outputStream ->
comicInfoFileStream.use { it.copyTo(outputStream) }
}
}
}
private fun setMangaDetailsFromComicInfoFile(stream: InputStream, manga: SManga) {
val comicInfo = AndroidXmlReader(stream, StandardCharsets.UTF_8.name()).use {
xml.decodeFromReader<ComicInfo>(it)
}
comicInfo.series?.let { manga.title = it.value }
comicInfo.writer?.let { manga.author = it.value }
comicInfo.summary?.let { manga.description = it.value }
listOfNotNull(
comicInfo.genre?.value,
comicInfo.tags?.value,
)
.flatMap { it.split(", ") }
.distinct()
.joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() }
?.let { manga.genre = it }
listOfNotNull(
comicInfo.penciller?.value,
comicInfo.inker?.value,
comicInfo.colorist?.value,
comicInfo.letterer?.value,
comicInfo.coverArtist?.value,
)
.flatMap { it.split(", ") }
.distinct()
.joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() }
?.let { manga.artist = it }
} }
@Serializable @Serializable
@ -172,7 +283,7 @@ class LocalSource(
val baseDirsFile = getBaseDirectoriesFiles(context) val baseDirsFile = getBaseDirectoriesFiles(context)
return getMangaDirsFiles(manga.url, baseDirsFile) return getMangaDirsFiles(manga.url, baseDirsFile)
// Only keep supported formats // Only keep supported formats
.filter { it.isDirectory || isSupportedFile(it.extension) } .filter { it.isDirectory || isSupportedArchiveFile(it.extension) }
.map { chapterFile -> .map { chapterFile ->
SChapter.create().apply { SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}" url = "${manga.url}/${chapterFile.name}"
@ -182,7 +293,6 @@ class LocalSource(
chapterFile.nameWithoutExtension chapterFile.nameWithoutExtension
} }
date_upload = chapterFile.lastModified() date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition.parseChapterNumber(manga.title, this.name, this.chapter_number) chapter_number = ChapterRecognition.parseChapterNumber(manga.title, this.name, this.chapter_number)
val format = getFormat(chapterFile) val format = getFormat(chapterFile)
@ -216,7 +326,7 @@ class LocalSource(
override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused") override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused")
// Miscellaneous // Miscellaneous
private fun isSupportedFile(extension: String): Boolean { private fun isSupportedArchiveFile(extension: String): Boolean {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
} }
@ -369,3 +479,4 @@ class LocalSource(
} }
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
private val COMIC_INFO_FILE = "ComicInfo.xml"

View file

@ -2,6 +2,7 @@
kotlin_version = "1.7.10" kotlin_version = "1.7.10"
coroutines_version = "1.6.4" coroutines_version = "1.6.4"
serialization_version = "1.4.0" serialization_version = "1.4.0"
xml_serialization_version = "0.84.2"
[libraries] [libraries]
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
@ -13,10 +14,12 @@ coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-androi
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" } serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" } serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" } serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xml_serialization_version" }
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-android", version.ref = "xml_serialization_version" }
[bundles] [bundles]
coroutines = ["coroutines-core", "coroutines-android"] coroutines = ["coroutines-core", "coroutines-android"]
serialization = ["serialization-json", "serialization-protobuf"] serialization = ["serialization-json", "serialization-protobuf", "serialization-xml-core", "serialization-xml"]
[plugins] [plugins]
android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version" } android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version" }