From 4e628fe6de7a691fbab7960c5f9b750e563eb8a1 Mon Sep 17 00:00:00 2001 From: Shamicen <84282253+Shamicen@users.noreply.github.com> Date: Fri, 11 Nov 2022 22:16:37 +0100 Subject: [PATCH] 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 * 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 Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com> --- .../eu/kanade/domain/manga/model/ComicInfo.kt | 31 ++++++- .../java/eu/kanade/tachiyomi/AppModule.kt | 5 ++ .../tachiyomi/data/download/Downloader.kt | 81 +++++++++++++++++++ .../eu/kanade/tachiyomi/source/LocalSource.kt | 10 +++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt b/app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt index 3d089debf..7962ff622 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt @@ -1,12 +1,14 @@ package eu.kanade.domain.manga.model import kotlinx.serialization.Serializable +import nl.adaptivity.xmlutil.serialization.XmlElement import nl.adaptivity.xmlutil.serialization.XmlSerialName import nl.adaptivity.xmlutil.serialization.XmlValue @Serializable @XmlSerialName("ComicInfo", "", "") data class ComicInfo( + val title: ComicInfoTitle?, val series: ComicInfoSeries?, val summary: ComicInfoSummary?, val writer: ComicInfoWriter?, @@ -15,9 +17,24 @@ data class ComicInfo( val colorist: ComicInfoColorist?, val letterer: ComicInfoLetterer?, val coverArtist: ComicInfoCoverArtist?, + val translator: ComicInfoTranslator?, val genre: ComicInfoGenre?, 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 @XmlSerialName("Series", "", "") @@ -51,6 +68,10 @@ data class ComicInfoLetterer(@XmlValue(true) val value: String = "") @XmlSerialName("CoverArtist", "", "") data class ComicInfoCoverArtist(@XmlValue(true) val value: String = "") +@Serializable +@XmlSerialName("Translator", "", "") +data class ComicInfoTranslator(@XmlValue(true) val value: String = "") + @Serializable @XmlSerialName("Genre", "", "") data class ComicInfoGenre(@XmlValue(true) val value: String = "") @@ -58,3 +79,11 @@ data class ComicInfoGenre(@XmlValue(true) val value: String = "") @Serializable @XmlSerialName("Tags", "", "") 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 = "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index f31d8343a..85e62ee6d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -43,6 +43,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.system.isDevFlavor import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory 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.XML import uy.kohesive.injekt.api.InjektModule @@ -106,6 +108,9 @@ class AppModule(val app: Application) : InjektModule { XML { unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() } autoPolymorphic = true + xmlDeclMode = XmlDeclMode.Charset + indent = 4 + xmlVersion = XmlVersion.XML10 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index a86709316..689d51245 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -6,7 +6,19 @@ import com.jakewharton.rxrelay.PublishRelay import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.toDbChapter 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.track.interactor.GetTracks +import eu.kanade.domain.track.model.Track import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.ChapterCache 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.UnmeteredSource 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.fetchAllImageUrlsFromPageList 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.logcat import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking import logcat.LogPriority +import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import rx.Observable import rx.android.schedulers.AndroidSchedulers @@ -36,8 +52,10 @@ import rx.schedulers.Schedulers import rx.subscriptions.CompositeSubscription import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import java.io.BufferedOutputStream import java.io.File +import java.io.FileOutputStream import java.util.zip.CRC32 import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -63,8 +81,14 @@ class Downloader( private val sourceManager: SourceManager = Injekt.get(), private val chapterCache: ChapterCache = 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. */ @@ -513,6 +537,8 @@ class Downloader( // 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")) } + createComicInfoFile(tmpDir, download.manga, download.chapter) + download.status = if (downloadedImages.size == download.pages!!.size) { // Only rename the directory if it's downloaded. if (downloadPreferences.saveChaptersAsCBZ().get()) { @@ -524,6 +550,8 @@ class Downloader( DiskUtil.createNoMediaFile(tmpDir, context) + createComicInfoFile(mangaDir, download.manga, download.chapter) + Download.State.DOWNLOADED } else { Download.State.ERROR @@ -564,6 +592,59 @@ class Downloader( 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 10910fafb..2f77f7216 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -269,6 +269,16 @@ class LocalSource( .joinToString(", ") { it.trim() } .takeIf { it.isNotEmpty() } ?.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