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
This commit is contained in:
@@ -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))
|
- 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))
|
- 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 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
|
### Improved
|
||||||
- Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
- Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
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 mihon.app.di.appGraph
|
||||||
import tachiyomi.core.common.preference.TriState
|
import tachiyomi.core.common.preference.TriState
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
@@ -13,6 +16,7 @@ import tachiyomi.domain.chapter.model.Chapter
|
|||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
// TODO: move these into the domain model
|
// TODO: move these into the domain model
|
||||||
val Manga.readingMode: Long
|
val Manga.readingMode: Long
|
||||||
@@ -85,30 +89,41 @@ fun getComicInfo(
|
|||||||
urls: List<String>,
|
urls: List<String>,
|
||||||
categories: List<String>?,
|
categories: List<String>?,
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
) = ComicInfo(
|
): ComicInfo {
|
||||||
title = ComicInfo.Title(chapter.name),
|
val date = chapter.dateUpload
|
||||||
series = ComicInfo.Series(manga.title),
|
.takeIf { it != 0L }
|
||||||
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
?.let {
|
||||||
if ((it.rem(1) == 0.0)) {
|
Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
ComicInfo.Number(it.toInt().toString())
|
|
||||||
} else {
|
|
||||||
ComicInfo.Number(it.toString())
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
web = ComicInfo.Web(urls.joinToString(" ")),
|
return ComicInfo(
|
||||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
title = ComicInfo.Title(chapter.name),
|
||||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
series = ComicInfo.Series(manga.title),
|
||||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
if ((it.rem(1) == 0.0)) {
|
||||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
ComicInfo.Number(it.toInt().toString())
|
||||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
} else {
|
||||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
ComicInfo.Number(it.toString())
|
||||||
),
|
}
|
||||||
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
},
|
||||||
source = ComicInfo.SourceMihon(sourceName),
|
web = ComicInfo.Web(urls.joinToString(" ")),
|
||||||
inker = null,
|
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||||
colorist = null,
|
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||||
letterer = null,
|
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||||
coverArtist = null,
|
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||||
tags = null,
|
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) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ fun SManga.getComicInfo() = ComicInfo(
|
|||||||
letterer = null,
|
letterer = null,
|
||||||
coverArtist = null,
|
coverArtist = null,
|
||||||
tags = null,
|
tags = null,
|
||||||
|
year = null,
|
||||||
|
month = null,
|
||||||
|
day = null,
|
||||||
categories = null,
|
categories = null,
|
||||||
source = null,
|
source = null,
|
||||||
)
|
)
|
||||||
@@ -80,6 +83,9 @@ data class ComicInfo(
|
|||||||
val genre: Genre?,
|
val genre: Genre?,
|
||||||
val tags: Tags?,
|
val tags: Tags?,
|
||||||
val web: Web?,
|
val web: Web?,
|
||||||
|
val year: Year?,
|
||||||
|
val month: Month?,
|
||||||
|
val day: Day?,
|
||||||
val publishingStatus: PublishingStatusTachiyomi?,
|
val publishingStatus: PublishingStatusTachiyomi?,
|
||||||
val categories: CategoriesTachiyomi?,
|
val categories: CategoriesTachiyomi?,
|
||||||
val source: SourceMihon?,
|
val source: SourceMihon?,
|
||||||
@@ -148,6 +154,18 @@ data class ComicInfo(
|
|||||||
@XmlSerialName("Web", "", "")
|
@XmlSerialName("Web", "", "")
|
||||||
data class Web(@XmlValue(true) val value: String = "")
|
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
|
// The spec doesn't have a good field for this
|
||||||
@Serializable
|
@Serializable
|
||||||
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
|
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
|
||||||
|
|||||||
@@ -31,4 +31,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.injekt)
|
implementation(libs.injekt)
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.supervisorScope
|
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.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
@@ -267,6 +270,19 @@ class LocalSource(
|
|||||||
comicInfo.title?.let { chapter.name = it.value }
|
comicInfo.title?.let { chapter.name = it.value }
|
||||||
comicInfo.number?.value?.toFloatOrNull()?.let { chapter.chapter_number = it }
|
comicInfo.number?.value?.toFloatOrNull()?.let { chapter.chapter_number = it }
|
||||||
comicInfo.translator?.let { chapter.scanlator = it.value }
|
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
|
// Chapters
|
||||||
|
|||||||
Reference in New Issue
Block a user