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 <Shamicen@users.noreply.github.com>
This commit is contained in:
arkon 2023-10-08 22:27:06 -04:00
parent b7d282235d
commit 79b37df647
3 changed files with 50 additions and 19 deletions

View File

@ -8,6 +8,27 @@ import nl.adaptivity.xmlutil.serialization.XmlValue
const val COMIC_INFO_FILE = "ComicInfo.xml" 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) { fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.series?.let { title = it.value } comicInfo.series?.let { title = it.value }
comicInfo.writer?.let { author = it.value } comicInfo.writer?.let { author = it.value }
@ -39,6 +60,8 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value) status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value)
} }
// https://anansi-project.github.io/docs/comicinfo/schemas/v2.0
@Suppress("UNUSED")
@Serializable @Serializable
@XmlSerialName("ComicInfo", "", "") @XmlSerialName("ComicInfo", "", "")
data class ComicInfo( data class ComicInfo(
@ -59,12 +82,10 @@ data class ComicInfo(
val publishingStatus: PublishingStatusTachiyomi?, val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?, val categories: CategoriesTachiyomi?,
) { ) {
@Suppress("UNUSED")
@XmlElement(false) @XmlElement(false)
@XmlSerialName("xmlns:xsd", "", "") @XmlSerialName("xmlns:xsd", "", "")
val xmlSchema: String = "http://www.w3.org/2001/XMLSchema" val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
@Suppress("UNUSED")
@XmlElement(false) @XmlElement(false)
@XmlSerialName("xmlns:xsi", "", "") @XmlSerialName("xmlns:xsi", "", "")
val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance" val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"

View File

@ -1,6 +1,7 @@
package tachiyomi.source.local package tachiyomi.source.local
import android.content.Context import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.UnmeteredSource 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.source.model.SManga
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import kotlinx.coroutines.runBlocking
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
@ -19,6 +19,7 @@ import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
@ -122,7 +123,6 @@ actual class LocalSource(
// Fetch chapters of all the manga // Fetch chapters of all the manga
mangas.forEach { manga -> mangas.forEach { manga ->
runBlocking {
val chapters = getChapterList(manga) val chapters = getChapterList(manga)
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
val chapter = chapters.last() val chapter = chapters.last()
@ -140,7 +140,6 @@ actual class LocalSource(
} }
} }
} }
}
return MangasPage(mangas.toList(), false) return MangasPage(mangas.toList(), false)
} }
@ -153,6 +152,7 @@ actual class LocalSource(
// Augment manga details based on metadata files // Augment manga details based on metadata files
try { try {
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList() val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
val comicInfoFile = mangaDirFiles val comicInfoFile = mangaDirFiles
@ -169,7 +169,8 @@ actual class LocalSource(
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga) 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 -> { legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run { json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
title?.let { manga.title = it } title?.let { manga.title = it }
@ -179,6 +180,16 @@ actual class LocalSource(
genre?.let { manga.genre = it.joinToString() } genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it } 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 // Copy ComicInfo.xml from chapter archive to top level if found
@ -187,7 +198,6 @@ actual class LocalSource(
.filter(Archive::isSupported) .filter(Archive::isSupported)
.toList() .toList()
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val folderPath = mangaDir?.absolutePath val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath) val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)

View File

@ -18,7 +18,7 @@ actual class LocalCoverManager(
actual fun find(mangaUrl: String): File? { actual fun find(mangaUrl: String): File? {
return fileSystem.getFilesInMangaDirectory(mangaUrl) 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) } .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image // Get the first actual image
.firstOrNull { .firstOrNull {