Add option to attempt to use first volume cover on MangaDex (#14031)

* Add option to attempt to use first volume cover on MangaDex.

* Fix missing first volume covers and remove logical symbols.

* Reinforce isOneShot check and reword preference.
This commit is contained in:
Alessandro Jean 2022-10-29 22:59:22 -03:00 committed by GitHub
parent c18a8d5860
commit eaa7b15bae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 45 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'MangaDex'
pkgNameSuffix = 'all.mangadex'
extClass = '.MangaDexFactory'
extVersionCode = 170
extVersionCode = 171
isNsfw = true
}

View File

@ -108,9 +108,18 @@ object MDConstants {
return "${hasSanitizedUuidsPref}_$dexLang"
}
private const val tryUsingFirstVolumeCoverPref = "tryUsingFirstVolumeCover"
const val tryUsingFirstVolumeCoverDefault = false
fun getTryUsingFirstVolumeCoverPrefKey(dexLang: String): String {
return "${tryUsingFirstVolumeCoverPref}_$dexLang"
}
private const val tagGroupContent = "content"
private const val tagGroupFormat = "format"
private const val tagGroupGenre = "genre"
private const val tagGroupTheme = "theme"
val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
const val tagAnthologyUuid = "51d83883-4103-437c-b4b1-731cb73d786c"
const val tagOneShotUuid = "0234a31e-a729-4e28-9d6a-3f87c4966b9e"
}

View File

@ -9,10 +9,13 @@ import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterListDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverListDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaListDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.RelationshipDto
@ -26,7 +29,6 @@ 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 kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import okhttp3.CacheControl
import okhttp3.Headers
@ -97,9 +99,10 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val hasMoreResults = mangaListDto.limit + mangaListDto.offset < mangaListDto.total
val coverSuffix = preferences.coverQuality
val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
val mangaList = mangaListDto.data.map { mangaDataDto ->
val fileName = mangaDataDto.relationships
val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
?.attributes?.fileName
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
@ -129,13 +132,14 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val mangaRequest = GET(mangaUrl.build().toString(), headers, CacheControl.FORCE_NETWORK)
val mangaResponse = client.newCall(mangaRequest).execute()
val mangaListDto = mangaResponse.parseAs<MangaListDto>()
val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
val mangaDtoMap = mangaListDto.data.associateBy({ it.id }, { it })
val coverSuffix = preferences.coverQuality
val mangaList = mangaIds.mapNotNull { mangaDtoMap[it] }.map { mangaDataDto ->
val fileName = mangaDataDto.relationships
val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
?.attributes?.fileName
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
@ -182,7 +186,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
.map { searchMangaListRequest(it, page) }
else ->
return super.fetchSearchManga(page, query, filters)
return super.fetchSearchManga(page, query.trim(), filters)
}
}
@ -292,11 +296,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
}
val mangaListDto = response.parseAs<MangaListDto>()
val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
val coverSuffix = preferences.coverQuality
val mangaList = mangaListDto.data.map { mangaDataDto ->
val fileName = mangaDataDto.relationships
val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
?.attributes?.fileName
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
@ -365,33 +370,74 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
return helper.createManga(
manga.data,
fetchSimpleChapterList(manga, dexLang),
fetchFirstVolumeCover(manga),
dexLang,
preferences.coverQuality
)
}
/**
* get a quick-n-dirty list of the chapters to be used in determining the manga status.
* uses the 'aggregate' endpoint
* Get a quick-n-dirty list of the chapters to be used in determining the manga status.
* Uses the 'aggregate' endpoint.
*
* @see MangaDexHelper.getPublicationStatus
* @see AggregateDto
*/
private fun fetchSimpleChapterList(manga: MangaDto, langCode: String): List<String> {
private fun fetchSimpleChapterList(manga: MangaDto, langCode: String): Map<String, AggregateVolume> {
val url = "${MDConstants.apiMangaUrl}/${manga.data.id}/aggregate?translatedLanguage[]=$langCode"
val response = client.newCall(GET(url, headers)).execute()
val chapters: AggregateDto
try {
chapters = response.parseAs()
} catch (e: SerializationException) {
return emptyList()
return runCatching { response.parseAs<AggregateDto>() }
.getOrNull()?.volumes.orEmpty()
}
/**
* Attempt to get the first volume cover if the setting is enabled.
* Uses the 'covers' endpoint.
*
* @see CoverListDto
*/
private fun fetchFirstVolumeCover(manga: MangaDto): String? {
return fetchFirstVolumeCovers(listOf(manga.data))?.get(manga.data.id)
}
/**
* Attempt to get the first volume cover if the setting is enabled.
* Uses the 'covers' endpoint.
*
* @see CoverListDto
*/
private fun fetchFirstVolumeCovers(mangaList: List<MangaDataDto>): Map<String, String>? {
if (!preferences.tryUsingFirstVolumeCover) {
return null
}
if (chapters.volumes.isNullOrEmpty()) return emptyList()
val mangaMap = mangaList.associate { it.id to it.attributes }
.filterValues { !it.originalLanguage.isNullOrEmpty() }
val locales = mangaList.mapNotNull { it.attributes.originalLanguage }.distinct()
val limit = (mangaMap.size * locales.size).coerceAtMost(100)
return chapters.volumes.values
.flatMap { it.chapters.values }
.map { it.chapter }
val apiUrl = "${MDConstants.apiUrl}/cover".toHttpUrl().newBuilder()
.addQueryParameter("order[volume]", "asc")
.addQueryParameter("manga[]", mangaMap.keys)
.addQueryParameter("locales[]", locales.toSet())
.addQueryParameter("limit", limit.toString())
.addQueryParameter("offset", "0")
.toString()
val result = runCatching {
client.newCall(GET(apiUrl, headers)).execute().parseAs<CoverListDto>().data
}
val covers = result.getOrNull() ?: return null
return covers
.groupBy { it.relationships.find { r -> r.type == MDConstants.manga }!!.id }
.mapValues {
it.value.find { c -> c.attributes?.locale == mangaMap[it.key]?.originalLanguage }
}
.filterValues { !it?.attributes?.fileName.isNullOrEmpty() }
.mapValues { it.value!!.attributes!!.fileName!! }
}
// Chapter list section
@ -525,6 +571,21 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
}
}
val tryUsingFirstVolumeCoverPref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang)
title = helper.intl.tryUsingFirstVolumeCover
summary = helper.intl.tryUsingFirstVolumeCoverSummary
setDefaultValue(MDConstants.tryUsingFirstVolumeCoverDefault)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit()
.putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue)
.commit()
}
}
val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getDataSaverPreferenceKey(dexLang)
title = helper.intl.dataSaver
@ -636,6 +697,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
}
screen.addPreference(coverQualityPref)
screen.addPreference(tryUsingFirstVolumeCoverPref)
screen.addPreference(dataSaverPref)
screen.addPreference(standardHttpsPortPref)
screen.addPreference(contentRatingPref)
@ -680,6 +742,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
private val SharedPreferences.coverQuality
get() = getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "")
private val SharedPreferences.tryUsingFirstVolumeCover
get() = getBoolean(
MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang),
MDConstants.tryUsingFirstVolumeCoverDefault
)
private val SharedPreferences.blockedGroups
get() = getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")
?.split(",")

View File

@ -5,18 +5,18 @@ import android.text.TextWatcher
import android.util.Log
import android.widget.Button
import android.widget.EditText
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDataDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaAttributesDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.asMdMap
import eu.kanade.tachiyomi.extension.all.mangadex.dto.toLocalizedString
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -27,7 +27,7 @@ import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
class MangaDexHelper(private val lang: String) {
class MangaDexHelper(lang: String) {
val mdFilters = MangaDexFilters()
@ -98,7 +98,11 @@ class MangaDexHelper(private val lang: String) {
* Maps dex status to Tachi status.
* Adapted from the MangaDex handler from TachiyomiSY.
*/
fun getPublicationStatus(attr: MangaAttributesDto, chapters: List<String>): Int {
fun getPublicationStatus(attr: MangaAttributesDto, volumes: Map<String, AggregateVolume>): Int {
val chaptersList = volumes.values
.flatMap { it.chapters.values }
.map { it.chapter }
val tempStatus = when (attr.status) {
"ongoing" -> SManga.ONGOING
"cancelled" -> SManga.CANCELLED
@ -110,10 +114,13 @@ class MangaDexHelper(private val lang: String) {
val publishedOrCancelled = tempStatus == SManga.PUBLISHING_FINISHED ||
tempStatus == SManga.CANCELLED
return if (chapters.contains(attr.lastChapter) && publishedOrCancelled) {
SManga.COMPLETED
} else {
tempStatus
val isOneShot = attr.tags.any { it.id == MDConstants.tagOneShotUuid } &&
attr.tags.none { it.id == MDConstants.tagAnthologyUuid }
return when {
chaptersList.contains(attr.lastChapter) && publishedOrCancelled -> SManga.COMPLETED
isOneShot && volumes["none"]?.chapters?.get("none") != null -> SManga.COMPLETED
else -> tempStatus
}
}
@ -210,15 +217,14 @@ class MangaDexHelper(private val lang: String) {
): SManga {
return SManga.create().apply {
url = "/manga/${mangaDataDto.id}"
val titleMap = mangaDataDto.attributes.title.asMdMap()
val titleMap = mangaDataDto.attributes.title.toLocalizedString()
val dirtyTitle = titleMap[lang]
?: titleMap["en"]
?: titleMap["ja-ro"]
?: mangaDataDto.attributes.altTitles.jsonArray
.find {
val altTitle = it.asMdMap()
(altTitle[lang] ?: altTitle["en"]) != null
}?.asMdMap()?.values?.singleOrNull()
?: mangaDataDto.attributes.altTitles
.map { it.toLocalizedString() }
.find { (it[lang] ?: it["en"]) !== null }
?.values?.singleOrNull()
?: titleMap["ja"] // romaji titles are sometimes ja (and are not altTitles)
?: titleMap.values.firstOrNull() // use literally anything from title as a last resort
title = cleanString(dirtyTitle ?: "")
@ -237,7 +243,8 @@ class MangaDexHelper(private val lang: String) {
*/
fun createManga(
mangaDataDto: MangaDataDto,
chapters: List<String>,
chapters: Map<String, AggregateVolume>,
firstVolumeCover: String?,
lang: String,
coverSuffix: String?
): SManga {
@ -275,7 +282,7 @@ class MangaDexHelper(private val lang: String) {
.mapNotNull { it.attributes!!.name }
.distinct()
val coverFileName = mangaDataDto.relationships
val coverFileName = firstVolumeCover ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
?.attributes?.fileName
@ -293,7 +300,7 @@ class MangaDexHelper(private val lang: String) {
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } +
nonGenres.filterNotNull()
val desc = attr.description.asMdMap()
val desc = attr.description.toLocalizedString()
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
description = cleanString(desc[lang] ?: desc["en"] ?: "")

View File

@ -96,18 +96,18 @@ class MangaDexIntl(lang: String) {
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
"Ative para fazer requisições em somente servidores de imagem que usem a porta 443. " +
"Isso permite com que usuários com regras mais restritas de firewall possam acessar " +
"as imagens do MangaDex."
"as imagens do $MANGADEX_NAME."
SPANISH_LATAM, SPANISH ->
"Habilite esta opción solicitar las imágenes a los servidores que usan el puerto 443. " +
"Esto permite a los usuarios con restricciones estrictas de firewall acceder " +
"a las imagenes en MangaDex"
"a las imagenes en $MANGADEX_NAME"
RUSSIAN ->
"Запрашивает изображения только с серверов которые используют порт 443. " +
"Это позволяет пользователям со строгими правилами брандмауэра загружать " +
"изображения с Mangadex."
"изображения с $MANGADEX_NAME."
else ->
"Enable to only request image servers that use port 443. This allows users with " +
"stricter firewall restrictions to access MangaDex images"
"stricter firewall restrictions to access $MANGADEX_NAME images"
}
val contentRating: String = when (availableLang) {
@ -265,6 +265,20 @@ class MangaDexIntl(lang: String) {
"Enter as a Comma-separated list of uploader UUIDs"
}
val tryUsingFirstVolumeCover: String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Tentar usar a capa do primeiro volume como capa"
else -> "Attempt to use the first volume cover as cover"
}
val tryUsingFirstVolumeCoverSummary: String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
"Pode ser necessário atualizar os itens já adicionados na biblioteca. " +
"Alternativamente, limpe o banco de dados para as novas capas aparecerem."
else ->
"May need to manually refresh entries already in library. " +
"Otherwise, clear database to have new covers to show up."
}
val publicationDemographic: String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Demografia da publicação"
SPANISH_LATAM, SPANISH -> "Demografía"
@ -748,7 +762,7 @@ class MangaDexIntl(lang: String) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Pós-apocalíptico"
SPANISH_LATAM, SPANISH -> "Post-Apocalíptico"
RUSSIAN -> "Постапокалиптика"
else -> "Post-Apocalypytic"
else -> "Post-Apocalyptic"
}
val themePsychological: String = when (availableLang) {

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import kotlinx.serialization.Serializable
@Serializable
data class CoverListDto(
val data: List<CoverDto> = emptyList()
)
@Serializable
data class CoverDto(
val id: String,
val attributes: CoverAttributesDto? = null,
val relationships: List<RelationshipDto> = emptyList()
)
@Serializable
data class CoverAttributesDto(
val name: String? = null,
val fileName: String? = null,
val locale: String? = null
)

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
@ -45,7 +46,7 @@ data class MangaDataDto(
@Serializable
data class MangaAttributesDto(
val title: JsonElement,
val altTitles: JsonElement,
val altTitles: JsonArray,
val description: JsonElement,
val originalLanguage: String?,
val lastVolume: String?,
@ -67,8 +68,14 @@ data class TagAttributesDto(
val group: String
)
fun JsonElement.asMdMap(): Map<String, String> {
return runCatching {
(this as JsonObject).map { it.key to (it.value.jsonPrimitive.contentOrNull ?: "") }.toMap()
}.getOrElse { emptyMap() }
typealias LocalizedString = Map<String, String>
/**
* Temporary workaround while Dex API still returns arrays instead of objects
* in the places that uses [LocalizedString].
*/
fun JsonElement.toLocalizedString(): LocalizedString {
return (this as? JsonObject)?.entries
?.associate { (key, value) -> key to (value.jsonPrimitive.contentOrNull ?: "") }
.orEmpty()
}