Эх сурвалжийг харах

Create ComicInfo Metadata files on chapter download (#8033)

* generate ComicInfo files at the chapter root and inside CBZ archives on chapter download.

* Update app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt

Co-authored-by: Andreas <[email protected]>

* Improvements suggested by @ghostbear

* now creates ComicInfo files in normal chapter folders as well
use manga directly instead of converting it to SManga
truncate old files before overwriting them

Co-authored-by: Andreas <[email protected]>

* remove empty line after resolving merge conflict

* fixes Serializer for class 'ComicInfo' is not found error

* some changes to comments and variable names

* Revert leftover changes to archiveChapter() function

* minor cleanup

* Changed Chapter to SChapter

Co-authored-by: Andreas <[email protected]>
Co-authored-by: Andreas <[email protected]>
Shamicen 2 жил өмнө
parent
commit
4e628fe6de

+ 30 - 1
app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt

@@ -1,12 +1,14 @@
 package eu.kanade.domain.manga.model
 
 import kotlinx.serialization.Serializable
+import nl.adaptivity.xmlutil.serialization.XmlElement
 import nl.adaptivity.xmlutil.serialization.XmlSerialName
 import nl.adaptivity.xmlutil.serialization.XmlValue
 
 @Serializable
 @XmlSerialName("ComicInfo", "", "")
 data class ComicInfo(
+    val title: ComicInfoTitle?,
     val series: ComicInfoSeries?,
     val summary: ComicInfoSummary?,
     val writer: ComicInfoWriter?,
@@ -15,9 +17,24 @@ data class ComicInfo(
     val colorist: ComicInfoColorist?,
     val letterer: ComicInfoLetterer?,
     val coverArtist: ComicInfoCoverArtist?,
+    val translator: ComicInfoTranslator?,
     val genre: ComicInfoGenre?,
     val tags: ComicInfoTags?,
-)
+    val web: ComicInfoWeb?,
+    val publishingStatusTachiyomi: ComicInfoPublishingStatusTachiyomi?,
+) {
+    @XmlElement(false)
+    @XmlSerialName("xmlns:xsd", "", "")
+    val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
+
+    @XmlElement(false)
+    @XmlSerialName("xmlns:xsi", "", "")
+    val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"
+}
+
+@Serializable
+@XmlSerialName("Title", "", "")
+data class ComicInfoTitle(@XmlValue(true) val value: String = "")
 
 @Serializable
 @XmlSerialName("Series", "", "")
@@ -51,6 +68,10 @@ data class ComicInfoLetterer(@XmlValue(true) val value: String = "")
 @XmlSerialName("CoverArtist", "", "")
 data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "")
 
+@Serializable
+@XmlSerialName("Translator", "", "")
+data class ComicInfoTranslator(@XmlValue(true) val value: String = "")
+
 @Serializable
 @XmlSerialName("Genre", "", "")
 data class ComicInfoGenre(@XmlValue(true) val value: String = "")
@@ -58,3 +79,11 @@ data class ComicInfoGenre(@XmlValue(true) val value: String = "")
 @Serializable
 @XmlSerialName("Tags", "", "")
 data class ComicInfoTags(@XmlValue(true) val value: String = "")
+
+@Serializable
+@XmlSerialName("Web", "", "")
+data class ComicInfoWeb(@XmlValue(true) val value: String = "")
+
+@Serializable
+@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
+data class ComicInfoPublishingStatusTachiyomi(@XmlValue(true) val value: String = "")

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt

@@ -43,6 +43,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
 import eu.kanade.tachiyomi.util.system.isDevFlavor
 import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
 import kotlinx.serialization.json.Json
+import nl.adaptivity.xmlutil.XmlDeclMode
+import nl.adaptivity.xmlutil.core.XmlVersion
 import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
 import nl.adaptivity.xmlutil.serialization.XML
 import uy.kohesive.injekt.api.InjektModule
@@ -106,6 +108,9 @@ class AppModule(val app: Application) : InjektModule {
             XML {
                 unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
                 autoPolymorphic = true
+                xmlDeclMode = XmlDeclMode.Charset
+                indent = 4
+                xmlVersion = XmlVersion.XML10
             }
         }
 

+ 81 - 0
app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

@@ -6,7 +6,19 @@ import com.jakewharton.rxrelay.PublishRelay
 import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.domain.chapter.model.toDbChapter
 import eu.kanade.domain.download.service.DownloadPreferences
+import eu.kanade.domain.manga.model.ComicInfo
+import eu.kanade.domain.manga.model.ComicInfoGenre
+import eu.kanade.domain.manga.model.ComicInfoPenciller
+import eu.kanade.domain.manga.model.ComicInfoPublishingStatusTachiyomi
+import eu.kanade.domain.manga.model.ComicInfoSeries
+import eu.kanade.domain.manga.model.ComicInfoSummary
+import eu.kanade.domain.manga.model.ComicInfoTitle
+import eu.kanade.domain.manga.model.ComicInfoTranslator
+import eu.kanade.domain.manga.model.ComicInfoWeb
+import eu.kanade.domain.manga.model.ComicInfoWriter
 import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.track.interactor.GetTracks
+import eu.kanade.domain.track.model.Track
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.cache.ChapterCache
 import eu.kanade.tachiyomi.data.download.model.Download
@@ -16,6 +28,8 @@ import eu.kanade.tachiyomi.data.notification.NotificationHandler
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.UnmeteredSource
 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.online.HttpSource
 import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
 import eu.kanade.tachiyomi.util.lang.RetryWithDelay
@@ -28,7 +42,9 @@ import eu.kanade.tachiyomi.util.storage.saveTo
 import eu.kanade.tachiyomi.util.system.ImageUtil
 import eu.kanade.tachiyomi.util.system.logcat
 import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
 import logcat.LogPriority
+import nl.adaptivity.xmlutil.serialization.XML
 import okhttp3.Response
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
@@ -36,8 +52,10 @@ import rx.schedulers.Schedulers
 import rx.subscriptions.CompositeSubscription
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
 import java.io.BufferedOutputStream
 import java.io.File
+import java.io.FileOutputStream
 import java.util.zip.CRC32
 import java.util.zip.ZipEntry
 import java.util.zip.ZipOutputStream
@@ -63,8 +81,14 @@ class Downloader(
     private val sourceManager: SourceManager = Injekt.get(),
     private val chapterCache: ChapterCache = Injekt.get(),
     private val downloadPreferences: DownloadPreferences = Injekt.get(),
+    private val getTracks: GetTracks = Injekt.get(),
 ) {
 
+    /**
+     * xml format used for ComicInfo files
+     */
+    private val xml: XML by injectLazy()
+
     /**
      * Store for persisting downloads across restarts.
      */
@@ -513,6 +537,8 @@ class Downloader(
         // Ensure that the chapter folder has all the images.
         val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) }
 
+        createComicInfoFile(tmpDir, download.manga, download.chapter)
+
         download.status = if (downloadedImages.size == download.pages!!.size) {
             // Only rename the directory if it's downloaded.
             if (downloadPreferences.saveChaptersAsCBZ().get()) {
@@ -524,6 +550,8 @@ class Downloader(
 
             DiskUtil.createNoMediaFile(tmpDir, context)
 
+            createComicInfoFile(mangaDir, download.manga, download.chapter)
+
             Download.State.DOWNLOADED
         } else {
             Download.State.ERROR
@@ -564,6 +592,59 @@ class Downloader(
         tmpDir.delete()
     }
 
+    /**
+     * Creates a ComicInfo.xml file inside the given directory.
+     *
+     * @param dir the directory in which the ComicInfo file will be generated.
+     * @param manga the manga of the chapter to download.
+     * @param chapter the chapter to download
+     */
+    private fun createComicInfoFile(
+        dir: UniFile,
+        manga: Manga,
+        chapter: SChapter,
+    ) {
+        File("${dir.filePath}/ComicInfo.xml").outputStream().also {
+            // Force overwrite old file
+            (it as? FileOutputStream)?.channel?.truncate(0)
+        }.use { it.write(getComicInfo(manga, chapter)) }
+    }
+
+    /**
+     * returns a ByteArray containing the Manga Metadata of the chapter to download in ComicInfo.xml format
+     *
+     * @param manga the manga of the chapter to download.
+     * @param chapter the name of the chapter to download
+     */
+    private fun getComicInfo(manga: Manga, chapter: SChapter): ByteArray {
+        val track: Track? = runBlocking { getTracks.await(manga.id).firstOrNull() }
+        val comicInfo = ComicInfo(
+            title = ComicInfoTitle(chapter.name),
+            series = ComicInfoSeries(manga.title),
+            summary = manga.description?.let { ComicInfoSummary(it) },
+            writer = manga.author?.let { ComicInfoWriter(it) },
+            penciller = manga.artist?.let { ComicInfoPenciller(it) },
+            translator = chapter.scanlator?.let { ComicInfoTranslator(it) },
+            genre = manga.genre?.let { ComicInfoGenre(it.joinToString()) },
+            web = track?.remoteUrl?.let { ComicInfoWeb(it) },
+            publishingStatusTachiyomi = when (manga.status) {
+                SManga.ONGOING.toLong() -> ComicInfoPublishingStatusTachiyomi("Ongoing")
+                SManga.COMPLETED.toLong() -> ComicInfoPublishingStatusTachiyomi("Completed")
+                SManga.LICENSED.toLong() -> ComicInfoPublishingStatusTachiyomi("Licensed")
+                SManga.PUBLISHING_FINISHED.toLong() -> ComicInfoPublishingStatusTachiyomi("Publishing finished")
+                SManga.CANCELLED.toLong() -> ComicInfoPublishingStatusTachiyomi("Cancelled")
+                SManga.ON_HIATUS.toLong() -> ComicInfoPublishingStatusTachiyomi("On hiatus")
+                else -> ComicInfoPublishingStatusTachiyomi("Unknown")
+            },
+            inker = null,
+            colorist = null,
+            letterer = null,
+            coverArtist = null,
+            tags = null,
+        )
+        return xml.encodeToString(ComicInfo.serializer(), comicInfo).toByteArray()
+    }
+
     /**
      * Completes a download. This method is called in the main thread.
      */

+ 10 - 0
app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt

@@ -269,6 +269,16 @@ class LocalSource(
             .joinToString(", ") { it.trim() }
             .takeIf { it.isNotEmpty() }
             ?.let { manga.artist = it }
+
+        manga.status = when (comicInfo.publishingStatusTachiyomi?.value) {
+            "Ongoing" -> SManga.ONGOING
+            "Completed" -> SManga.COMPLETED
+            "Licensed" -> SManga.LICENSED
+            "Publishing finished" -> SManga.PUBLISHING_FINISHED
+            "Cancelled" -> SManga.CANCELLED
+            "On hiatus" -> SManga.ON_HIATUS
+            else -> SManga.UNKNOWN
+        }
     }
 
     @Serializable