From a7179805ad1e7b3c3a0a4098560d04fed24b2701 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Wed, 16 Sep 2026 04:23:25 +0200 Subject: [PATCH] Use & write Year, Month, Day info in ComicInfo (#3967) * Use & write Year, Month, Day info in ComicInfo Currently, users can only modify the date Mihon displays in the chapter list by manipulating the _last modified date_ of the file. This is obviously inconvenient, especially since the ComicInfo spec includes Year, Month, and Day fields for this exact purpose. If present, this reads the date at start of day in the current default system time zone (which matches how we use Chapter.dateUpload with relativeDateText, for example). If Year is not specified, the entire date process is skipped. If Month is not specified, it is set to January. If Day is not specified, it is set to the first. The parsed date (if any) is set as the Chapter's `date_upload` which is used for display in the app. Additionally, Mihon now includes this info in the written ComicInfo.xml file for downloaded chapters. This will be convenient for many more users, not least of which those who move source-downloaded chapters to Local Source to read from there. --- I used kotlinx.datetime because it's what we use for date parsing in non-i18n places already. * Changelog --- CHANGELOG.md | 1 + .../eu/kanade/domain/manga/model/Manga.kt | 67 ++++++++++++------- .../core/metadata/comicinfo/ComicInfo.kt | 18 +++++ source-local/build.gradle.kts | 2 + .../tachiyomi/source/local/LocalSource.kt | 16 +++++ 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4318f5151..6d8cb0027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya)) ([#3776](https://github.com/mihonapp/mihon/pull/3776)) - Allow `id:` to search for slugs on Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792)) - Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818)) +- Add support for the Year, Month, and Day fields in ComicInfo.xml files for chapter dating ([@MajorTanya](https://github.com/MajorTanya)) ([#3967](https://github.com/mihonapp/mihon/pull/3967)) ### Improved - Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772)) diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index 5eb4fed81..a3c03ad35 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -5,6 +5,9 @@ import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode +import kotlinx.datetime.TimeZone +import kotlinx.datetime.number +import kotlinx.datetime.toLocalDateTime import mihon.app.di.appGraph import tachiyomi.core.common.preference.TriState import tachiyomi.core.metadata.comicinfo.ComicInfo @@ -13,6 +16,7 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.time.Instant // TODO: move these into the domain model val Manga.readingMode: Long @@ -85,30 +89,41 @@ fun getComicInfo( urls: List, categories: List?, sourceName: String, -) = ComicInfo( - title = ComicInfo.Title(chapter.name), - series = ComicInfo.Series(manga.title), - number = chapter.chapterNumber.takeIf { it >= 0 }?.let { - if ((it.rem(1) == 0.0)) { - ComicInfo.Number(it.toInt().toString()) - } else { - ComicInfo.Number(it.toString()) +): ComicInfo { + val date = chapter.dateUpload + .takeIf { it != 0L } + ?.let { + Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.currentSystemDefault()) } - }, - web = ComicInfo.Web(urls.joinToString(" ")), - summary = manga.description?.let { ComicInfo.Summary(it) }, - writer = manga.author?.let { ComicInfo.Writer(it) }, - penciller = manga.artist?.let { ComicInfo.Penciller(it) }, - translator = chapter.scanlator?.let { ComicInfo.Translator(it) }, - genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) }, - publishingStatus = ComicInfo.PublishingStatusTachiyomi( - ComicInfoPublishingStatus.toComicInfoValue(manga.status), - ), - categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) }, - source = ComicInfo.SourceMihon(sourceName), - inker = null, - colorist = null, - letterer = null, - coverArtist = null, - tags = null, -) + + return ComicInfo( + title = ComicInfo.Title(chapter.name), + series = ComicInfo.Series(manga.title), + number = chapter.chapterNumber.takeIf { it >= 0 }?.let { + if ((it.rem(1) == 0.0)) { + ComicInfo.Number(it.toInt().toString()) + } else { + ComicInfo.Number(it.toString()) + } + }, + web = ComicInfo.Web(urls.joinToString(" ")), + summary = manga.description?.let { ComicInfo.Summary(it) }, + writer = manga.author?.let { ComicInfo.Writer(it) }, + penciller = manga.artist?.let { ComicInfo.Penciller(it) }, + translator = chapter.scanlator?.let { ComicInfo.Translator(it) }, + genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) }, + publishingStatus = ComicInfo.PublishingStatusTachiyomi( + ComicInfoPublishingStatus.toComicInfoValue(manga.status), + ), + categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) }, + source = ComicInfo.SourceMihon(sourceName), + inker = null, + colorist = null, + letterer = null, + coverArtist = null, + tags = null, + year = date?.year?.let { ComicInfo.Year(it) }, + month = date?.month?.number?.let { ComicInfo.Month(it) }, + day = date?.day?.let { ComicInfo.Day(it) }, + ) +} diff --git a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt index 5d4d92277..c522394ab 100644 --- a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt +++ b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt @@ -26,6 +26,9 @@ fun SManga.getComicInfo() = ComicInfo( letterer = null, coverArtist = null, tags = null, + year = null, + month = null, + day = null, categories = null, source = null, ) @@ -80,6 +83,9 @@ data class ComicInfo( val genre: Genre?, val tags: Tags?, val web: Web?, + val year: Year?, + val month: Month?, + val day: Day?, val publishingStatus: PublishingStatusTachiyomi?, val categories: CategoriesTachiyomi?, val source: SourceMihon?, @@ -148,6 +154,18 @@ data class ComicInfo( @XmlSerialName("Web", "", "") data class Web(@XmlValue(true) val value: String = "") + @Serializable + @XmlSerialName("Year", "", "") + data class Year(@XmlValue(true) val value: Int = -1) + + @Serializable + @XmlSerialName("Month", "", "") + data class Month(@XmlValue(true) val value: Int = -1) + + @Serializable + @XmlSerialName("Day", "", "") + data class Day(@XmlValue(true) val value: Int = -1) + // The spec doesn't have a good field for this @Serializable @XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty") diff --git a/source-local/build.gradle.kts b/source-local/build.gradle.kts index a8f6d3b3e..a5075b496 100644 --- a/source-local/build.gradle.kts +++ b/source-local/build.gradle.kts @@ -31,4 +31,6 @@ dependencies { implementation(libs.injekt) implementation(libs.jsoup) + + implementation(libs.kotlinx.datetime) } diff --git a/source-local/src/main/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/main/kotlin/tachiyomi/source/local/LocalSource.kt index 28e111861..1ccb356b9 100644 --- a/source-local/src/main/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/main/kotlin/tachiyomi/source/local/LocalSource.kt @@ -17,6 +17,9 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.supervisorScope +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority @@ -267,6 +270,19 @@ class LocalSource( comicInfo.title?.let { chapter.name = it.value } comicInfo.number?.value?.toFloatOrNull()?.let { chapter.chapter_number = it } comicInfo.translator?.let { chapter.scanlator = it.value } + + // only bother with partial dates if the year is not null, abandon date parsing otherwise + val year = comicInfo.year?.value?.takeIf { it > 0 }?.toString()?.padStart(2, '0') ?: return + val month = (comicInfo.month?.value?.coerceIn(1, 31) ?: 1).toString().padStart(2, '0') + val day = (comicInfo.day?.value?.coerceIn(1, 31) ?: 1).toString().padStart(2, '0') + + val dateInstant = try { + LocalDate.parse("$year-$month-$day").atStartOfDayIn(TimeZone.currentSystemDefault()) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + null + } + dateInstant?.let { chapter.date_upload = it.toEpochMilliseconds() } } // Chapters