From d6a58d5d3417a906e6eb497e05b65bce71b46e04 Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Sat, 27 Jan 2024 13:11:13 +0800 Subject: [PATCH] Generate ComicInfo.xml on download --- .../com/hippo/ehviewer/client/EhEngine.kt | 2 +- .../com/hippo/ehviewer/spider/ComicInfo.kt | 122 ++++++++++++++++++ .../com/hippo/ehviewer/spider/SpiderQueen.kt | 24 +++- gradle/libs.versions.toml | 13 +- 4 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt index a0155cec4..6ac208204 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt @@ -372,7 +372,7 @@ object EhEngine { } }.fetchUsingAsText(String::parseAs) - suspend fun fillGalleryListByApi(galleryInfoList: List, referer: String) = + suspend fun fillGalleryListByApi(galleryInfoList: List, referer: String? = null) = galleryInfoList.chunked(MAX_REQUEST_SIZE).chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk -> if (index != 0) { delay(REQUEST_INTERVAL) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt new file mode 100644 index 000000000..15902151a --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt @@ -0,0 +1,122 @@ +package com.hippo.ehviewer.spider + +import com.hippo.ehviewer.client.EhTagDatabase +import com.hippo.ehviewer.client.EhUrl +import com.hippo.ehviewer.client.data.GalleryInfo +import com.hippo.unifile.UniFile +import com.hippo.unifile.openOutputStream +import java.io.OutputStreamWriter +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import nl.adaptivity.xmlutil.XmlDeclMode +import nl.adaptivity.xmlutil.core.XmlVersion +import nl.adaptivity.xmlutil.newGenericWriter +import nl.adaptivity.xmlutil.serialization.XML +import nl.adaptivity.xmlutil.serialization.XmlElement +import nl.adaptivity.xmlutil.xmlStreaming + +const val COMIC_INFO_FILE = "ComicInfo.xml" + +private val xml = XML { + recommended { + ignoreUnknownChildren() + } + xmlDeclMode = XmlDeclMode.Charset + xmlVersion = XmlVersion.XML10 +} + +fun GalleryInfo.getComicInfo(): ComicInfo { + val artists = mutableListOf() + val groups = mutableListOf() + val characters = mutableListOf() + val parodies = mutableListOf() + val tags = mutableListOf() + simpleTags?.forEach { + val (namespace, tag) = it.split(':', limit = 2) + when (namespace) { + "artist", "cosplayer" -> artists.add(tag) + "group" -> groups.add(tag) + "character" -> characters.add(tag) + "parody" -> if (tag != "original") parodies.add(tag) + "language", "reclass" -> Unit + else -> tags.add("${EhTagDatabase.namespaceToPrefix(namespace)}:$tag") + } + } + return ComicInfo( + series = title, + alternateSeries = titleJpn.takeUnless { it.isNullOrBlank() }, + writer = groups.takeUnless { it.isEmpty() }?.joinToString(), + penciller = artists.takeUnless { it.isEmpty() }?.joinToString(), + genre = tags.takeUnless { it.isEmpty() }?.joinToString(), + web = EhUrl.getGalleryDetailUrl(gid, token), + pageCount = pages, + languageISO = simpleLanguage?.lowercase(), + characters = characters.takeUnless { it.isEmpty() }?.joinToString(), + teams = parodies.takeUnless { it.isEmpty() }?.joinToString(), + communityRating = "%.1f".format(rating), + ) +} + +fun ComicInfo.write(file: UniFile) { + file.openOutputStream().use { + it.channel.truncate(0) + OutputStreamWriter(it, Charsets.UTF_8).use { writer -> + xmlStreaming.newGenericWriter(writer).use { xmlWriter -> + xml.encodeToWriter(xmlWriter, ComicInfo.serializer(), this) + } + } + } +} + +@Serializable +data class ComicInfo( + @XmlElement + @SerialName("Series") + val series: String?, + + @XmlElement + @SerialName("AlternateSeries") + val alternateSeries: String?, + + @XmlElement + @SerialName("Writer") + val writer: String?, + + @XmlElement + @SerialName("Penciller") + val penciller: String?, + + @XmlElement + @SerialName("Genre") + val genre: String?, + + @XmlElement + @SerialName("Web") + val web: String?, + + @XmlElement + @SerialName("PageCount") + val pageCount: Int, + + @XmlElement + @SerialName("LanguageISO") + val languageISO: String?, + + @XmlElement + @SerialName("Characters") + val characters: String?, + + @XmlElement + @SerialName("Teams") + val teams: String?, + + @XmlElement + @SerialName("CommunityRating") + val communityRating: String?, +) { + @SerialName("xmlns:xsi") + val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance" + + @SerialName("xsi:noNamespaceSchemaLocation") + val xmlSchemaLocation: String = "https://raw.githubusercontent.com/anansi-project/comicinfo/main/schema/v2.0/ComicInfo.xsd" +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt index fee96fd8a..ba8c65a3f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt @@ -41,6 +41,7 @@ import com.hippo.ehviewer.image.Image import com.hippo.ehviewer.util.ExceptionUtils import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import java.util.concurrent.atomic.AtomicInteger import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -431,6 +432,21 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS mSpiderInfo.saveToCache() } + private suspend fun writeComicInfo() { + mSpiderDen.downloadDir?.run { + createFile(COMIC_INFO_FILE)?.also { + runCatching { + withNonCancellableContext { + EhEngine.fillGalleryListByApi(listOf(galleryInfo)) + } + galleryInfo.getComicInfo().write(it) + }.onFailure { + it.printStackTrace() + } + } + } + } + private fun isStateDone(state: Int): Boolean { return state == STATE_FINISHED || state == STATE_FAILED } @@ -493,7 +509,13 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS val gid = galleryInfo.gid return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply { setMode(mode) - launchIO { updateMode() } + launchIO { + // Will create download dir if not exists + updateMode() + if (mode == MODE_DOWNLOAD) { + writeComicInfo() + } + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d23b7e778..4858b1a0b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ ktor = "3.0.0-beta-1" okhttp3 = "5.0.0-alpha.12" okio = "3.7.0" splitties = "3.0.0" +xmlutil = "0.86.3" [libraries] aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" } @@ -92,9 +93,6 @@ jsoup = "org.jsoup:jsoup:1.17.2" kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } -kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } -kotlinx-serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } @@ -110,6 +108,13 @@ r8 = { module = "com.android.tools:r8", version.ref = "android-tools-r8" } reorderable = "sh.calvin.reorderable:reorderable:1.2.0" +serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" } +serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization" } + +serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xmlutil" } +serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-android", version.ref = "xmlutil" } + splitties-appctx = { module = "com.louiscad.splitties:splitties-appctx", version.ref = "splitties" } splitties-arch-room = { module = "com.louiscad.splitties:splitties-arch-room", version.ref = "splitties" } splitties-preferences = { module = "com.louiscad.splitties:splitties-preferences", version.ref = "splitties" } @@ -120,7 +125,7 @@ androidx-navigation = ["androidx-navigation-compose", "androidx-navigation-ui"] coil = ["coil-compose", "coil-gif", "coil-network"] compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-windowsizeclass", "compose-ui-util"] cronet = ["cronet-api", "cronet-gms"] -kotlinx-serialization = ["kotlinx-serialization-cbor", "kotlinx-serialization-json", "kotlinx-serialization-json-okio"] +kotlinx-serialization = ["serialization-cbor", "serialization-json", "serialization-json-okio", "serialization-xml-core", "serialization-xml"] ktor = ["ktor-client-okhttp"] okhttp = ["okhttp-android"] splitties = ["splitties-appctx", "splitties-arch-room", "splitties-preferences", "splitties-systemservices"]