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"
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"

View File

@ -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)

View File

@ -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 {