ソースを参照

Automatically convert details.json to ComicInfo.xml for local series

Originally contributed as #9603
I ended up coming back to this since it seems like a reasonable way to migrate
users in the short-medium term. We'll remove this in a later release.

Co-authored-by: Shamicen <[email protected]>
arkon 1 年間 前
コミット
79b37df647

+ 23 - 2
core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt

@@ -8,6 +8,27 @@ import nl.adaptivity.xmlutil.serialization.XmlValue
 
 const val COMIC_INFO_FILE = "ComicInfo.xml"
 
+fun SManga.getComicInfo() = ComicInfo(
+    series = ComicInfo.Series(title),
+    summary = description?.let { ComicInfo.Summary(it) },
+    writer = author?.let { ComicInfo.Writer(it) },
+    penciller = artist?.let { ComicInfo.Penciller(it) },
+    genre = genre?.let { ComicInfo.Genre(it) },
+    publishingStatus = ComicInfo.PublishingStatusTachiyomi(
+        ComicInfoPublishingStatus.toComicInfoValue(status.toLong()),
+    ),
+    title = null,
+    number = null,
+    web = null,
+    translator = null,
+    inker = null,
+    colorist = null,
+    letterer = null,
+    coverArtist = null,
+    tags = null,
+    categories = null,
+)
+
 fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
     comicInfo.series?.let { title = it.value }
     comicInfo.writer?.let { author = it.value }
@@ -39,6 +60,8 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
     status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value)
 }
 
+// https://anansi-project.github.io/docs/comicinfo/schemas/v2.0
+@Suppress("UNUSED")
 @Serializable
 @XmlSerialName("ComicInfo", "", "")
 data class ComicInfo(
@@ -59,12 +82,10 @@ data class ComicInfo(
     val publishingStatus: PublishingStatusTachiyomi?,
     val categories: CategoriesTachiyomi?,
 ) {
-    @Suppress("UNUSED")
     @XmlElement(false)
     @XmlSerialName("xmlns:xsd", "", "")
     val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
 
-    @Suppress("UNUSED")
     @XmlElement(false)
     @XmlSerialName("xmlns:xsi", "", "")
     val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"

+ 26 - 16
source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt

@@ -1,6 +1,7 @@
 package tachiyomi.source.local
 
 import android.content.Context
+import com.hippo.unifile.UniFile
 import eu.kanade.tachiyomi.source.CatalogueSource
 import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.UnmeteredSource
@@ -10,7 +11,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
 import eu.kanade.tachiyomi.source.model.SManga
 import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
 import eu.kanade.tachiyomi.util.storage.EpubFile
-import kotlinx.coroutines.runBlocking
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.decodeFromStream
 import logcat.LogPriority
@@ -19,6 +19,7 @@ import nl.adaptivity.xmlutil.serialization.XML
 import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
 import tachiyomi.core.metadata.comicinfo.ComicInfo
 import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
+import tachiyomi.core.metadata.comicinfo.getComicInfo
 import tachiyomi.core.metadata.tachiyomi.MangaDetails
 import tachiyomi.core.util.lang.withIOContext
 import tachiyomi.core.util.system.ImageUtil
@@ -122,22 +123,20 @@ actual class LocalSource(
 
         // Fetch chapters of all the manga
         mangas.forEach { manga ->
-            runBlocking {
-                val chapters = getChapterList(manga)
-                if (chapters.isNotEmpty()) {
-                    val chapter = chapters.last()
-                    val format = getFormat(chapter)
+            val chapters = getChapterList(manga)
+            if (chapters.isNotEmpty()) {
+                val chapter = chapters.last()
+                val format = getFormat(chapter)
 
-                    if (format is Format.Epub) {
-                        EpubFile(format.file).use { epub ->
-                            epub.fillMangaMetadata(manga)
-                        }
+                if (format is Format.Epub) {
+                    EpubFile(format.file).use { epub ->
+                        epub.fillMangaMetadata(manga)
                     }
+                }
 
-                    // Copy the cover from the first chapter found if not available
-                    if (manga.thumbnail_url == null) {
-                        updateCover(chapter, manga)
-                    }
+                // Copy the cover from the first chapter found if not available
+                if (manga.thumbnail_url == null) {
+                    updateCover(chapter, manga)
                 }
             }
         }
@@ -153,6 +152,7 @@ actual class LocalSource(
 
         // Augment manga details based on metadata files
         try {
+            val mangaDir = fileSystem.getMangaDirectory(manga.url)
             val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
 
             val comicInfoFile = mangaDirFiles
@@ -169,7 +169,8 @@ actual class LocalSource(
                     setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
                 }
 
-                // TODO: automatically convert these to ComicInfo.xml
+                // Old custom JSON format
+                // TODO: remove support for this entirely after a while
                 legacyJsonDetailsFile != null -> {
                     json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
                         title?.let { manga.title = it }
@@ -179,6 +180,16 @@ actual class LocalSource(
                         genre?.let { manga.genre = it.joinToString() }
                         status?.let { manga.status = it }
                     }
+                    // Replace with ComicInfo.xml file
+                    val comicInfo = manga.getComicInfo()
+                    UniFile.fromFile(mangaDir)
+                        ?.createFile(COMIC_INFO_FILE)
+                        ?.openOutputStream()
+                        ?.use {
+                            val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
+                            it.write(comicInfoString.toByteArray())
+                            legacyJsonDetailsFile.delete()
+                        }
                 }
 
                 // Copy ComicInfo.xml from chapter archive to top level if found
@@ -187,7 +198,6 @@ actual class LocalSource(
                         .filter(Archive::isSupported)
                         .toList()
 
-                    val mangaDir = fileSystem.getMangaDirectory(manga.url)
                     val folderPath = mangaDir?.absolutePath
 
                     val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)

+ 1 - 1
source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt

@@ -18,7 +18,7 @@ actual class LocalCoverManager(
 
     actual fun find(mangaUrl: String): File? {
         return fileSystem.getFilesInMangaDirectory(mangaUrl)
-            // Get all file whose names start with 'cover'
+            // Get all file whose names start with "cover"
             .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
             // Get the first actual image
             .firstOrNull {