From 79b37df647049df90921cae1fabdca2312404784 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 8 Oct 2023 22:27:06 -0400 Subject: [PATCH] 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 --- .../core/metadata/comicinfo/ComicInfo.kt | 25 ++++++++++- .../tachiyomi/source/local/LocalSource.kt | 42 ++++++++++++------- .../source/local/image/LocalCoverManager.kt | 2 +- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt index 06cce7ccce..681b4819b1 100644 --- a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt +++ b/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" diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index b74dcbb2d1..636d205f2e 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/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(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) diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index e5aaf5dd7b..7683756e38 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/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 {