Create ComicInfo Metadata files on chapter download (#8033)

* generate ComicInfo files at the chapter root and inside CBZ archives on chapter download.

* Update app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt

Co-authored-by: Andreas <andreas.everos@gmail.com>

* Improvements suggested by @ghostbear

* now creates ComicInfo files in normal chapter folders as well
use manga directly instead of converting it to SManga
truncate old files before overwriting them

Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>

* remove empty line after resolving merge conflict

* fixes Serializer for class 'ComicInfo' is not found error

* some changes to comments and variable names

* Revert leftover changes to archiveChapter() function

* minor cleanup

* Changed Chapter to SChapter

Co-authored-by: Andreas <andreas.everos@gmail.com>
Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>
This commit is contained in:
Shamicen 2022-11-11 22:16:37 +01:00 committed by GitHub
parent a8eebd824a
commit 4e628fe6de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 1 deletions

View file

@ -1,12 +1,14 @@
package eu.kanade.domain.manga.model package eu.kanade.domain.manga.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlSerialName import nl.adaptivity.xmlutil.serialization.XmlSerialName
import nl.adaptivity.xmlutil.serialization.XmlValue import nl.adaptivity.xmlutil.serialization.XmlValue
@Serializable @Serializable
@XmlSerialName("ComicInfo", "", "") @XmlSerialName("ComicInfo", "", "")
data class ComicInfo( data class ComicInfo(
val title: ComicInfoTitle?,
val series: ComicInfoSeries?, val series: ComicInfoSeries?,
val summary: ComicInfoSummary?, val summary: ComicInfoSummary?,
val writer: ComicInfoWriter?, val writer: ComicInfoWriter?,
@ -15,9 +17,24 @@ data class ComicInfo(
val colorist: ComicInfoColorist?, val colorist: ComicInfoColorist?,
val letterer: ComicInfoLetterer?, val letterer: ComicInfoLetterer?,
val coverArtist: ComicInfoCoverArtist?, val coverArtist: ComicInfoCoverArtist?,
val translator: ComicInfoTranslator?,
val genre: ComicInfoGenre?, val genre: ComicInfoGenre?,
val tags: ComicInfoTags?, val tags: ComicInfoTags?,
) val web: ComicInfoWeb?,
val publishingStatusTachiyomi: ComicInfoPublishingStatusTachiyomi?,
) {
@XmlElement(false)
@XmlSerialName("xmlns:xsd", "", "")
val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
@XmlElement(false)
@XmlSerialName("xmlns:xsi", "", "")
val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"
}
@Serializable
@XmlSerialName("Title", "", "")
data class ComicInfoTitle(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Series", "", "") @XmlSerialName("Series", "", "")
@ -51,6 +68,10 @@ data class ComicInfoLetterer(@XmlValue(true) val value: String = "")
@XmlSerialName("CoverArtist", "", "") @XmlSerialName("CoverArtist", "", "")
data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "") data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Translator", "", "")
data class ComicInfoTranslator(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Genre", "", "") @XmlSerialName("Genre", "", "")
data class ComicInfoGenre(@XmlValue(true) val value: String = "") data class ComicInfoGenre(@XmlValue(true) val value: String = "")
@ -58,3 +79,11 @@ data class ComicInfoGenre(@XmlValue(true) val value: String = "")
@Serializable @Serializable
@XmlSerialName("Tags", "", "") @XmlSerialName("Tags", "", "")
data class ComicInfoTags(@XmlValue(true) val value: String = "") data class ComicInfoTags(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Web", "", "")
data class ComicInfoWeb(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
data class ComicInfoPublishingStatusTachiyomi(@XmlValue(true) val value: String = "")

View file

@ -43,6 +43,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isDevFlavor
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.UnknownChildHandler import nl.adaptivity.xmlutil.serialization.UnknownChildHandler
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
@ -106,6 +108,9 @@ class AppModule(val app: Application) : InjektModule {
XML { XML {
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() } unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
autoPolymorphic = true autoPolymorphic = true
xmlDeclMode = XmlDeclMode.Charset
indent = 4
xmlVersion = XmlVersion.XML10
} }
} }

View file

@ -6,7 +6,19 @@ import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.toDbChapter import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.download.service.DownloadPreferences import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.manga.model.ComicInfo
import eu.kanade.domain.manga.model.ComicInfoGenre
import eu.kanade.domain.manga.model.ComicInfoPenciller
import eu.kanade.domain.manga.model.ComicInfoPublishingStatusTachiyomi
import eu.kanade.domain.manga.model.ComicInfoSeries
import eu.kanade.domain.manga.model.ComicInfoSummary
import eu.kanade.domain.manga.model.ComicInfoTitle
import eu.kanade.domain.manga.model.ComicInfoTranslator
import eu.kanade.domain.manga.model.ComicInfoWeb
import eu.kanade.domain.manga.model.ComicInfoWriter
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.model.Track
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -16,6 +28,8 @@ import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.lang.RetryWithDelay import eu.kanade.tachiyomi.util.lang.RetryWithDelay
@ -28,7 +42,9 @@ import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -36,8 +52,10 @@ import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
@ -63,8 +81,14 @@ class Downloader(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val chapterCache: ChapterCache = Injekt.get(), private val chapterCache: ChapterCache = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
) { ) {
/**
* xml format used for ComicInfo files
*/
private val xml: XML by injectLazy()
/** /**
* Store for persisting downloads across restarts. * Store for persisting downloads across restarts.
*/ */
@ -513,6 +537,8 @@ class Downloader(
// Ensure that the chapter folder has all the images. // Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) } val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) }
createComicInfoFile(tmpDir, download.manga, download.chapter)
download.status = if (downloadedImages.size == download.pages!!.size) { download.status = if (downloadedImages.size == download.pages!!.size) {
// Only rename the directory if it's downloaded. // Only rename the directory if it's downloaded.
if (downloadPreferences.saveChaptersAsCBZ().get()) { if (downloadPreferences.saveChaptersAsCBZ().get()) {
@ -524,6 +550,8 @@ class Downloader(
DiskUtil.createNoMediaFile(tmpDir, context) DiskUtil.createNoMediaFile(tmpDir, context)
createComicInfoFile(mangaDir, download.manga, download.chapter)
Download.State.DOWNLOADED Download.State.DOWNLOADED
} else { } else {
Download.State.ERROR Download.State.ERROR
@ -564,6 +592,59 @@ class Downloader(
tmpDir.delete() tmpDir.delete()
} }
/**
* Creates a ComicInfo.xml file inside the given directory.
*
* @param dir the directory in which the ComicInfo file will be generated.
* @param manga the manga of the chapter to download.
* @param chapter the chapter to download
*/
private fun createComicInfoFile(
dir: UniFile,
manga: Manga,
chapter: SChapter,
) {
File("${dir.filePath}/ComicInfo.xml").outputStream().also {
// Force overwrite old file
(it as? FileOutputStream)?.channel?.truncate(0)
}.use { it.write(getComicInfo(manga, chapter)) }
}
/**
* returns a ByteArray containing the Manga Metadata of the chapter to download in ComicInfo.xml format
*
* @param manga the manga of the chapter to download.
* @param chapter the name of the chapter to download
*/
private fun getComicInfo(manga: Manga, chapter: SChapter): ByteArray {
val track: Track? = runBlocking { getTracks.await(manga.id).firstOrNull() }
val comicInfo = ComicInfo(
title = ComicInfoTitle(chapter.name),
series = ComicInfoSeries(manga.title),
summary = manga.description?.let { ComicInfoSummary(it) },
writer = manga.author?.let { ComicInfoWriter(it) },
penciller = manga.artist?.let { ComicInfoPenciller(it) },
translator = chapter.scanlator?.let { ComicInfoTranslator(it) },
genre = manga.genre?.let { ComicInfoGenre(it.joinToString()) },
web = track?.remoteUrl?.let { ComicInfoWeb(it) },
publishingStatusTachiyomi = when (manga.status) {
SManga.ONGOING.toLong() -> ComicInfoPublishingStatusTachiyomi("Ongoing")
SManga.COMPLETED.toLong() -> ComicInfoPublishingStatusTachiyomi("Completed")
SManga.LICENSED.toLong() -> ComicInfoPublishingStatusTachiyomi("Licensed")
SManga.PUBLISHING_FINISHED.toLong() -> ComicInfoPublishingStatusTachiyomi("Publishing finished")
SManga.CANCELLED.toLong() -> ComicInfoPublishingStatusTachiyomi("Cancelled")
SManga.ON_HIATUS.toLong() -> ComicInfoPublishingStatusTachiyomi("On hiatus")
else -> ComicInfoPublishingStatusTachiyomi("Unknown")
},
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
)
return xml.encodeToString(ComicInfo.serializer(), comicInfo).toByteArray()
}
/** /**
* Completes a download. This method is called in the main thread. * Completes a download. This method is called in the main thread.
*/ */

View file

@ -269,6 +269,16 @@ class LocalSource(
.joinToString(", ") { it.trim() } .joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.let { manga.artist = it } ?.let { manga.artist = it }
manga.status = when (comicInfo.publishingStatusTachiyomi?.value) {
"Ongoing" -> SManga.ONGOING
"Completed" -> SManga.COMPLETED
"Licensed" -> SManga.LICENSED
"Publishing finished" -> SManga.PUBLISHING_FINISHED
"Cancelled" -> SManga.CANCELLED
"On hiatus" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
} }
@Serializable @Serializable