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:
parent
b7d282235d
commit
79b37df647
3 changed files with 50 additions and 19 deletions
|
@ -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"
|
||||||
|
|
|
@ -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,22 +123,20 @@ 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()
|
val format = getFormat(chapter)
|
||||||
val format = getFormat(chapter)
|
|
||||||
|
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file).use { epub ->
|
||||||
epub.fillMangaMetadata(manga)
|
epub.fillMangaMetadata(manga)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Copy the cover from the first chapter found if not available
|
// Copy the cover from the first chapter found if not available
|
||||||
if (manga.thumbnail_url == null) {
|
if (manga.thumbnail_url == null) {
|
||||||
updateCover(chapter, manga)
|
updateCover(chapter, manga)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Reference in a new issue