Update MangaDex to use API v5.7.5 (#14087)

* Update MangaDex to use API v5.7.5.

* Remove unused line.

* Fix list type string.

* Make usage of `authorOrArtist` in deeplink.

* Use proper custom serializer for `LocalizedString`.

* Use actual enums for manga properties.

* Fix list search not working.

Co-authored-by: nicki <72807749+curche@users.noreply.github.com>

Co-authored-by: nicki <72807749+curche@users.noreply.github.com>
This commit is contained in:
Alessandro Jean 2022-11-03 11:32:36 -03:00 committed by GitHub
parent 6dff9d8cfa
commit bd4faa01d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 368 additions and 236 deletions

View File

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

View File

@ -12,12 +12,15 @@ object MDConstants {
const val mangaLimit = 20
const val latestChapterLimit = 100
const val chapter = "chapter"
const val manga = "manga"
const val coverArt = "cover_art"
const val scanlator = "scanlation_group"
const val uploader = "user"
const val scanlationGroup = "scanlation_group"
const val user = "user"
const val author = "author"
const val artist = "artist"
const val tag = "tag"
const val list = "custom_list"
const val legacyNoGroupId = "00e03853-1b96-4f41-9542-c71b8692033b"
const val cdnUrl = "https://uploads.mangadex.org"

View File

@ -13,12 +13,12 @@ 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.CoverArtDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtListDto
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
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -103,7 +103,8 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val mangaList = mangaListDto.data.map { mangaDataDto ->
val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
.filterIsInstance<CoverArtDto>()
.firstOrNull()
?.attributes?.fileName
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
}
@ -118,7 +119,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val mangaIds = chapterListDto.data
.flatMap { it.relationships }
.filter { it.type == MDConstants.manga }
.filterIsInstance<MangaDataDto>()
.map { it.id }
.distinct()
.toSet()
@ -140,7 +141,8 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val mangaList = mangaIds.mapNotNull { mangaDtoMap[it] }.map { mangaDataDto ->
val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
.filterIsInstance<CoverArtDto>()
.firstOrNull()
?.attributes?.fileName
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
}
@ -162,6 +164,8 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
MDConstants.defaultBlockedGroups + preferences.blockedGroups
)
.addQueryParameter("excludedUploaders[]", preferences.blockedUploaders)
.addQueryParameter("includeFuturePublishAt", "0")
.addQueryParameter("includeEmptyPages", "0")
return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
}
@ -169,24 +173,39 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
// SEARCH section
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
when {
return when {
query.startsWith(MDConstants.prefixChSearch) ->
return getMangaIdFromChapterId(query.removePrefix(MDConstants.prefixChSearch)).flatMap { manga_id ->
super.fetchSearchManga(page, MDConstants.prefixIdSearch + manga_id, filters)
}
getMangaIdFromChapterId(query.removePrefix(MDConstants.prefixChSearch))
.flatMap { mangaId ->
super.fetchSearchManga(
page = page,
query = MDConstants.prefixIdSearch + mangaId,
filters = filters
)
}
query.startsWith(MDConstants.prefixUsrSearch) ->
return client.newCall(searchMangaUploaderRequest(page, query.removePrefix(MDConstants.prefixUsrSearch)))
client
.newCall(
request = searchMangaUploaderRequest(
page = page,
uploader = query.removePrefix(MDConstants.prefixUsrSearch)
)
)
.asObservableSuccess()
.map { latestUpdatesParse(it) }
query.startsWith(MDConstants.prefixListSearch) ->
return client.newCall(GET(MDConstants.apiListUrl + "/" + query.removePrefix(MDConstants.prefixListSearch), headers, CacheControl.FORCE_NETWORK))
client
.newCall(
request = searchMangaListRequest(
list = query.removePrefix(MDConstants.prefixListSearch)
)
)
.asObservableSuccess()
.map { searchMangaListRequest(it, page) }
.map { searchMangaListParse(it, page) }
else ->
return super.fetchSearchManga(page, query.trim(), filters)
else -> super.fetchSearchManga(page, query.trim(), filters)
}
}
@ -198,8 +217,9 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
throw Exception(helper.intl.unableToProcessChapterRequest(response.code))
}
response.parseAs<ChapterDto>().data.relationships
.find { it.type == MDConstants.manga }!!.id
response.parseAs<ChapterDto>().data!!.relationships
.filterIsInstance<MangaDataDto>()
.firstOrNull()!!.id
}
}
@ -223,23 +243,21 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
}
query.startsWith(MDConstants.prefixGrpSearch) -> {
val groupID = query.removePrefix(MDConstants.prefixGrpSearch)
if (!helper.containsUuid(groupID)) {
val groupId = query.removePrefix(MDConstants.prefixGrpSearch)
if (!helper.containsUuid(groupId)) {
throw Exception(helper.intl.invalidGroupId)
}
tempUrl.addQueryParameter("group", groupID)
tempUrl.addQueryParameter("group", groupId)
}
query.startsWith(MDConstants.prefixAuthSearch) -> {
val authorID = query.removePrefix(MDConstants.prefixAuthSearch)
if (!helper.containsUuid(authorID)) {
val authorId = query.removePrefix(MDConstants.prefixAuthSearch)
if (!helper.containsUuid(authorId)) {
throw Exception(helper.intl.invalidAuthorId)
}
tempUrl
.addQueryParameter("authors[]", authorID)
.addQueryParameter("artists[]", authorID)
tempUrl.addQueryParameter("authorOrArtist", authorId)
}
else -> {
@ -262,9 +280,13 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
private fun searchMangaListRequest(response: Response, page: Int): MangasPage {
private fun searchMangaListRequest(list: String): Request {
return GET("${MDConstants.apiListUrl}/$list", headers, CacheControl.FORCE_NETWORK)
}
private fun searchMangaListParse(response: Response, page: Int): MangasPage {
val listDto = response.parseAs<ListDto>()
val listDtoFiltered = listDto.data.relationships.filter { it.type != "Manga" }
val listDtoFiltered = listDto.data!!.relationships.filterIsInstance<MangaDataDto>()
val amount = listDtoFiltered.count()
if (amount < 1) {
@ -280,7 +302,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val ids = listDtoFiltered
.filterIndexed { i, _ -> i >= minIndex && i < (minIndex + MDConstants.mangaLimit) }
.map(RelationshipDto::id)
.map(MangaDataDto::id)
.toSet()
url.addQueryParameter("ids[]", ids)
@ -306,7 +328,8 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val mangaList = mangaListDto.data.map { mangaDataDto ->
val fileName = firstVolumeCovers[mangaDataDto.id] ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
.filterIsInstance<CoverArtDto>()
.firstOrNull()
?.attributes?.fileName
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
}
@ -321,6 +344,8 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
.addQueryParameter("translatedLanguage[]", dexLang)
.addQueryParameter("order[publishAt]", "desc")
.addQueryParameter("includeFutureUpdates", "0")
.addQueryParameter("includeFuturePublishAt", "0")
.addQueryParameter("includeEmptyPages", "0")
.addQueryParameter("uploader", uploader)
.addQueryParameter("originalLanguage[]", preferences.originalLanguages)
.addQueryParameter("contentRating[]", preferences.contentRating)
@ -372,7 +397,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
val manga = response.parseAs<MangaDto>()
return helper.createManga(
manga.data,
manga.data!!,
fetchSimpleChapterList(manga, dexLang),
fetchFirstVolumeCover(manga),
dexLang,
@ -388,7 +413,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
* @see AggregateDto
*/
private fun fetchSimpleChapterList(manga: MangaDto, langCode: String): Map<String, AggregateVolume> {
val url = "${MDConstants.apiMangaUrl}/${manga.data.id}/aggregate?translatedLanguage[]=$langCode"
val url = "${MDConstants.apiMangaUrl}/${manga.data!!.id}/aggregate?translatedLanguage[]=$langCode"
val response = client.newCall(GET(url, headers)).execute()
return runCatching { response.parseAs<AggregateDto>() }
@ -399,26 +424,26 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
* Attempt to get the first volume cover if the setting is enabled.
* Uses the 'covers' endpoint.
*
* @see CoverListDto
* @see CoverArtListDto
*/
private fun fetchFirstVolumeCover(manga: MangaDto): String? {
return fetchFirstVolumeCovers(listOf(manga.data))?.get(manga.data.id)
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
* @see CoverArtListDto
*/
private fun fetchFirstVolumeCovers(mangaList: List<MangaDataDto>): Map<String, String>? {
if (!preferences.tryUsingFirstVolumeCover || mangaList.isEmpty()) {
return null
}
val mangaMap = mangaList.associate { it.id to it.attributes }
val mangaMap = mangaList.associate { it.id to it.attributes!! }
.filterValues { !it.originalLanguage.isNullOrEmpty() }
val locales = mangaList.mapNotNull { it.attributes.originalLanguage }.distinct()
val locales = mangaList.mapNotNull { it.attributes!!.originalLanguage }.distinct()
val limit = (mangaMap.size * locales.size).coerceAtMost(100)
val apiUrl = "${MDConstants.apiUrl}/cover".toHttpUrl().newBuilder()
@ -430,13 +455,16 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
.toString()
val result = runCatching {
client.newCall(GET(apiUrl, headers)).execute().parseAs<CoverListDto>().data
client.newCall(GET(apiUrl, headers)).execute().parseAs<CoverArtListDto>().data
}
val covers = result.getOrNull() ?: return null
return covers
.groupBy { it.relationships.find { r -> r.type == MDConstants.manga }!!.id }
.groupBy {
it.relationships.filterIsInstance<MangaDataDto>()
.firstOrNull()!!.id
}
.mapValues {
it.value.find { c -> c.attributes?.locale == mangaMap[it.key]?.originalLanguage }
}
@ -502,16 +530,13 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
hasMoreResults = (limit + offset) < newChapterList.total
}
val now = Date().time
return chapterListResults
.mapNotNull { helper.createChapter(it) }
.filter { it.date_upload <= now }
return chapterListResults.mapNotNull(helper::createChapter)
} catch (e: Exception) {
Log.e("MangaDex", "error parsing chapter list", e)
throw e
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (!helper.containsUuid(chapter.url)) {
throw Exception(helper.intl.migrateWarning)

View File

@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.extension.all.mangadex
import android.content.SharedPreferences
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ContentRatingDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.PublicationDemographicDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.StatusDto
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl
@ -108,21 +111,17 @@ class MangaDexFilters {
)
return listOf(
ContentRating(intl.contentRatingSafe, MDConstants.contentRatingPrefValSafe).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValSafe) ?: true
ContentRating(intl.contentRatingSafe, ContentRatingDto.SAFE.value).apply {
state = contentRatings?.contains(MDConstants.contentRatingPrefValSafe) ?: true
},
ContentRating(intl.contentRatingSuggestive, MDConstants.contentRatingPrefValSuggestive).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true
ContentRating(intl.contentRatingSuggestive, ContentRatingDto.SUGGESTIVE.value).apply {
state = contentRatings?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true
},
ContentRating(intl.contentRatingErotica, MDConstants.contentRatingPrefValErotica).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValErotica) ?: false
ContentRating(intl.contentRatingErotica, ContentRatingDto.EROTICA.value).apply {
state = contentRatings?.contains(MDConstants.contentRatingPrefValErotica) ?: false
},
ContentRating(intl.contentRatingPornographic, MDConstants.contentRatingPrefValPornographic).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValPornographic) ?: false
ContentRating(intl.contentRatingPornographic, ContentRatingDto.PORNOGRAPHIC.value).apply {
state = contentRatings?.contains(MDConstants.contentRatingPrefValPornographic) ?: false
},
)
}
@ -142,11 +141,11 @@ class MangaDexFilters {
}
private fun getDemographics(intl: MangaDexIntl) = listOf(
Demographic(intl.publicationDemographicNone, "none"),
Demographic(intl.publicationDemographicShounen, "shounen"),
Demographic(intl.publicationDemographicShoujo, "shoujo"),
Demographic(intl.publicationDemographicSeinen, "seinen"),
Demographic(intl.publicationDemographicJosei, "josei")
Demographic(intl.publicationDemographicNone, PublicationDemographicDto.NONE.value),
Demographic(intl.publicationDemographicShounen, PublicationDemographicDto.SHOUNEN.value),
Demographic(intl.publicationDemographicShoujo, PublicationDemographicDto.SHOUJO.value),
Demographic(intl.publicationDemographicSeinen, PublicationDemographicDto.SEINEN.value),
Demographic(intl.publicationDemographicJosei, PublicationDemographicDto.JOSEI.value)
)
private class Status(name: String, val value: String) : Filter.CheckBox(name)
@ -164,10 +163,10 @@ class MangaDexFilters {
}
private fun getStatus(intl: MangaDexIntl) = listOf(
Status(intl.statusOngoing, "ongoing"),
Status(intl.statusCompleted, "completed"),
Status(intl.statusHiatus, "hiatus"),
Status(intl.statusCancelled, "cancelled"),
Status(intl.statusOngoing, StatusDto.ONGOING.value),
Status(intl.statusCompleted, StatusDto.COMPLETED.value),
Status(intl.statusHiatus, StatusDto.HIATUS.value),
Status(intl.statusCancelled, StatusDto.CANCELLED.value),
)
data class Sortable(val title: String, val value: String) {

View File

@ -6,17 +6,38 @@ 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.ArtistDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AttributesDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AuthorArtistAttributesDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AuthorDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterAttributesDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDataDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ContentRatingDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtAttributesDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.EntityDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListAttributesDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDataDto
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.toLocalizedString
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ScanlationGroupAttributes
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ScanlationGroupDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.StatusDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.TagAttributesDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.TagDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.UserAttributes
import eu.kanade.tachiyomi.extension.all.mangadex.dto.UserDto
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.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -35,8 +56,31 @@ class MangaDexHelper(lang: String) {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
prettyPrint = true
serializersModule += SerializersModule {
polymorphic(EntityDto::class) {
subclass(AuthorDto::class)
subclass(ArtistDto::class)
subclass(ChapterDataDto::class)
subclass(CoverArtDto::class)
subclass(ListDataDto::class)
subclass(MangaDataDto::class)
subclass(ScanlationGroupDto::class)
subclass(TagDto::class)
subclass(UserDto::class)
}
polymorphic(AttributesDto::class) {
subclass(AuthorArtistAttributesDto::class)
subclass(ChapterAttributesDto::class)
subclass(CoverArtAttributesDto::class)
subclass(ListAttributesDto::class)
subclass(MangaAttributesDto::class)
subclass(ScanlationGroupAttributes::class)
subclass(TagAttributesDto::class)
subclass(UserAttributes::class)
}
}
}
val intl = MangaDexIntl(lang)
@ -51,13 +95,15 @@ class MangaDexHelper(lang: String) {
*/
fun getChapterEndpoint(mangaId: String, offset: Int, langCode: String) =
"${MDConstants.apiMangaUrl}/$mangaId/feed".toHttpUrl().newBuilder()
.addQueryParameter("includes[]", MDConstants.scanlator)
.addQueryParameter("includes[]", MDConstants.uploader)
.addQueryParameter("includes[]", MDConstants.scanlationGroup)
.addQueryParameter("includes[]", MDConstants.user)
.addQueryParameter("limit", "500")
.addQueryParameter("offset", offset.toString())
.addQueryParameter("translatedLanguage[]", langCode)
.addQueryParameter("order[volume]", "desc")
.addQueryParameter("order[chapter]", "desc")
.addQueryParameter("includeFuturePublishAt", "0")
.addQueryParameter("includeEmptyPages", "0")
.toString()
/**
@ -104,10 +150,10 @@ class MangaDexHelper(lang: String) {
.map { it.chapter }
val tempStatus = when (attr.status) {
"ongoing" -> SManga.ONGOING
"cancelled" -> SManga.CANCELLED
"completed" -> SManga.PUBLISHING_FINISHED
"hiatus" -> SManga.ON_HIATUS
StatusDto.ONGOING -> SManga.ONGOING
StatusDto.CANCELLED -> SManga.CANCELLED
StatusDto.COMPLETED -> SManga.PUBLISHING_FINISHED
StatusDto.HIATUS -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
@ -217,12 +263,11 @@ class MangaDexHelper(lang: String) {
): SManga {
return SManga.create().apply {
url = "/manga/${mangaDataDto.id}"
val titleMap = mangaDataDto.attributes.title.toLocalizedString()
val titleMap = mangaDataDto.attributes!!.title
val dirtyTitle = titleMap[lang]
?: titleMap["en"]
?: titleMap["ja-ro"]
?: 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)
@ -249,58 +294,46 @@ class MangaDexHelper(lang: String) {
coverSuffix: String?
): SManga {
try {
val attr = mangaDataDto.attributes
val attr = mangaDataDto.attributes!!
// things that will go with the genre tags but aren't actually genre
val tempContentRating = attr.contentRating
val contentRating =
if (tempContentRating == null || tempContentRating.equals("safe", true)) {
null
} else {
intl.contentRatingGenre(tempContentRating)
}
val dexLocale = Locale.forLanguageTag(lang)
val nonGenres = listOf(
(attr.publicationDemographic ?: "")
.replaceFirstChar { it.uppercase(Locale.US) },
contentRating,
Locale(attr.originalLanguage ?: "")
.getDisplayLanguage(dexLocale)
.replaceFirstChar { it.uppercase(dexLocale) }
val nonGenres = listOfNotNull(
attr.publicationDemographic?.let { intl.publicationDemographic(it) },
attr.contentRating
.takeIf { it != ContentRatingDto.SAFE }
?.let { intl.contentRatingGenre(it) },
attr.originalLanguage
?.let { Locale.forLanguageTag(it) }
?.getDisplayName(dexLocale)
?.replaceFirstChar { it.uppercase(dexLocale) }
)
val authors = mangaDataDto.relationships
.filter { it.type.equals(MDConstants.author, true) }
.mapNotNull { it.attributes!!.name }
.filterIsInstance<AuthorDto>()
.mapNotNull { it.attributes?.name }
.distinct()
val artists = mangaDataDto.relationships
.filter { it.type.equals(MDConstants.artist, true) }
.mapNotNull { it.attributes!!.name }
.filterIsInstance<ArtistDto>()
.mapNotNull { it.attributes?.name }
.distinct()
val coverFileName = firstVolumeCover ?: mangaDataDto.relationships
.firstOrNull { it.type.equals(MDConstants.coverArt, true) }
.filterIsInstance<CoverArtDto>()
.firstOrNull()
?.attributes?.fileName
val tags = mdFilters.getTags(intl)
val tags = mdFilters.getTags(intl).associate { it.id to it.name }
val genresMap = attr.tags
.groupBy({ it.attributes.group }) { tagDto ->
tags.firstOrNull { it.id == tagDto.id }?.name
}
.map { (group, tags) ->
group to tags.filterNotNull().sortedWith(intl.collator)
}
.toMap()
.groupBy({ it.attributes!!.group }) { tagDto -> tags[tagDto.id] }
.mapValues { it.value.filterNotNull().sortedWith(intl.collator) }
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } +
nonGenres.filterNotNull()
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
val desc = attr.description.toLocalizedString()
val desc = attr.description
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
description = cleanString(desc[lang] ?: desc["en"] ?: "")
@ -322,18 +355,18 @@ class MangaDexHelper(lang: String) {
*/
fun createChapter(chapterDataDto: ChapterDataDto): SChapter? {
try {
val attr = chapterDataDto.attributes
val attr = chapterDataDto.attributes!!
val groups = chapterDataDto.relationships
.filter { it.type.equals(MDConstants.scanlator, true) }
.filterIsInstance<ScanlationGroupDto>()
.filterNot { it.id == MDConstants.legacyNoGroupId } // 'no group' left over from MDv3
.mapNotNull { it.attributes!!.name }
.mapNotNull { it.attributes?.name }
.joinToString(" & ")
.ifEmpty {
// fall back to uploader name if no group
val users = chapterDataDto.relationships
.filter { it.type.equals(MDConstants.uploader, true) }
.mapNotNull { it.attributes!!.username }
.filterIsInstance<UserDto>()
.mapNotNull { it.attributes?.username }
if (users.isNotEmpty()) intl.uploadedBy(users) else ""
}
.ifEmpty { intl.noGroup } // "No Group" as final resort
@ -407,9 +440,13 @@ class MangaDexHelper(lang: String) {
fun setupEditTextUuidValidator(editText: EditText) {
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Do nothing.
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Do nothing.
}
override fun afterTextChanged(editable: Editable?) {
requireNotNull(editable)

View File

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.extension.all.mangadex
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ContentRatingDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.PublicationDemographicDto
import java.text.Collator
import java.util.Locale
@ -162,18 +164,18 @@ class MangaDexIntl(lang: String) {
else -> "Pornographic"
}
private val contentRatingMap: Map<String, String> = mapOf(
"safe" to contentRatingSafe,
"suggestive" to contentRatingSuggestive,
"erotica" to contentRatingErotica,
"pornographic" to contentRatingPornographic
private val contentRatingMap: Map<ContentRatingDto, String> = mapOf(
ContentRatingDto.SAFE to contentRatingSafe,
ContentRatingDto.SUGGESTIVE to contentRatingSuggestive,
ContentRatingDto.EROTICA to contentRatingErotica,
ContentRatingDto.PORNOGRAPHIC to contentRatingPornographic
)
fun contentRatingGenre(contentRatingKey: String): String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação: ${contentRatingMap[contentRatingKey]}"
SPANISH_LATAM, SPANISH -> "Clasificación: ${contentRatingMap[contentRatingKey]}"
RUSSIAN -> "Рейтинг контента: ${contentRatingMap[contentRatingKey]}"
else -> "$contentRating: ${contentRatingMap[contentRatingKey]}"
fun contentRatingGenre(contentRating: ContentRatingDto): String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação: ${contentRatingMap[contentRating]}"
SPANISH_LATAM, SPANISH -> "Clasificación: ${contentRatingMap[contentRating]}"
RUSSIAN -> "Рейтинг контента: ${contentRatingMap[contentRating]}"
else -> "${this.contentRating}: ${contentRatingMap[contentRating]}"
}
val originalLanguage: String = when (availableLang) {
@ -313,6 +315,14 @@ class MangaDexIntl(lang: String) {
else -> "Josei"
}
fun publicationDemographic(demographic: PublicationDemographicDto): String = when (demographic) {
PublicationDemographicDto.NONE -> publicationDemographicNone
PublicationDemographicDto.SHOUNEN -> publicationDemographicShounen
PublicationDemographicDto.SHOUJO -> publicationDemographicShoujo
PublicationDemographicDto.SEINEN -> publicationDemographicSeinen
PublicationDemographicDto.JOSEI -> publicationDemographicJosei
}
val status: String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Estado"
SPANISH_LATAM, SPANISH -> "Estado"

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName(MDConstants.author)
data class AuthorDto(override val attributes: AuthorArtistAttributesDto? = null) : EntityDto()
@Serializable
@SerialName(MDConstants.artist)
data class ArtistDto(override val attributes: AuthorArtistAttributesDto? = null) : EntityDto()
@Serializable
data class AuthorArtistAttributesDto(val name: String) : AttributesDto()

View File

@ -1,28 +1,16 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ChapterListDto(
val limit: Int,
val offset: Int,
val total: Int,
val data: List<ChapterDataDto>,
)
typealias ChapterListDto = PaginatedResponseDto<ChapterDataDto>
typealias ChapterDto = ResponseDto<ChapterDataDto>
@Serializable
data class ChapterDto(
val result: String,
val data: ChapterDataDto,
)
@Serializable
data class ChapterDataDto(
val id: String,
val type: String,
val attributes: ChapterAttributesDto,
val relationships: List<RelationshipDto>,
)
@SerialName(MDConstants.chapter)
data class ChapterDataDto(override val attributes: ChapterAttributesDto? = null) : EntityDto()
@Serializable
data class ChapterAttributesDto(
@ -32,4 +20,4 @@ data class ChapterAttributesDto(
val pages: Int,
val publishAt: String,
val externalUrl: String?,
)
) : AttributesDto()

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias CoverArtListDto = PaginatedResponseDto<CoverArtDto>
@Serializable
@SerialName(MDConstants.coverArt)
data class CoverArtDto(override val attributes: CoverArtAttributesDto? = null) : EntityDto()
@Serializable
data class CoverArtAttributesDto(
val fileName: String? = null,
val locale: String? = null
) : AttributesDto()

View File

@ -1,22 +0,0 @@
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

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import kotlinx.serialization.Serializable
@Serializable
abstract class EntityDto {
val id: String = ""
val relationships: List<EntityDto> = emptyList()
abstract val attributes: AttributesDto?
}
@Serializable
abstract class AttributesDto

View File

@ -1,25 +1,18 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ListDto(
val result: String,
val response: String,
val data: ListDataDto,
)
typealias ListDto = ResponseDto<ListDataDto>
@Serializable
data class ListDataDto(
val id: String,
val type: String,
val attributes: ListAttributesDto,
val relationships: List<RelationshipDto>,
)
@SerialName(MDConstants.list)
data class ListDataDto(override val attributes: ListAttributesDto? = null) : EntityDto()
@Serializable
data class ListAttributesDto(
val name: String,
val visibility: String,
val version: Int,
)
) : AttributesDto()

View File

@ -1,81 +1,90 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.serializer
typealias MangaListDto = PaginatedResponseDto<MangaDataDto>
typealias MangaDto = ResponseDto<MangaDataDto>
@Serializable
data class MangaListDto(
val limit: Int,
val offset: Int,
val total: Int,
val data: List<MangaDataDto>,
)
@Serializable
data class MangaDto(
val result: String,
val data: MangaDataDto,
)
@Serializable
data class RelationshipDto(
val id: String,
val type: String,
val attributes: IncludesAttributesDto? = null,
)
@Serializable
data class IncludesAttributesDto(
val name: String? = null,
val fileName: String? = null,
val username: String? = null
)
@Serializable
data class MangaDataDto(
val id: String,
val type: String,
val attributes: MangaAttributesDto,
val relationships: List<RelationshipDto>,
)
@SerialName(MDConstants.manga)
data class MangaDataDto(override val attributes: MangaAttributesDto? = null) : EntityDto()
@Serializable
data class MangaAttributesDto(
val title: JsonElement,
val altTitles: JsonArray,
val description: JsonElement,
val title: LocalizedString,
val altTitles: List<LocalizedString>,
val description: LocalizedString,
val originalLanguage: String?,
val lastVolume: String?,
val lastChapter: String?,
val contentRating: String?,
val publicationDemographic: String?,
val status: String?,
val contentRating: ContentRatingDto? = null,
val publicationDemographic: PublicationDemographicDto? = null,
val status: StatusDto? = null,
val tags: List<TagDto>,
)
) : AttributesDto()
@Serializable
data class TagDto(
val id: String,
val attributes: TagAttributesDto
)
enum class ContentRatingDto(val value: String) {
@SerialName("safe") SAFE("safe"),
@SerialName("suggestive") SUGGESTIVE("suggestive"),
@SerialName("erotica") EROTICA("erotica"),
@SerialName("pornographic") PORNOGRAPHIC("pornographic")
}
@Serializable
data class TagAttributesDto(
val group: String
)
enum class PublicationDemographicDto(val value: String) {
@SerialName("none") NONE("none"),
@SerialName("shounen") SHOUNEN("shounen"),
@SerialName("shoujo") SHOUJO("shoujo"),
@SerialName("josei") JOSEI("josei"),
@SerialName("seinen") SEINEN("seinen")
}
typealias LocalizedString = Map<String, String>
@Serializable
enum class StatusDto(val value: String) {
@SerialName("ongoing") ONGOING("ongoing"),
@SerialName("completed") COMPLETED("completed"),
@SerialName("hiatus") HIATUS("hiatus"),
@SerialName("cancelled") CANCELLED("cancelled")
}
@Serializable
@SerialName(MDConstants.tag)
data class TagDto(override val attributes: TagAttributesDto? = null) : EntityDto()
@Serializable
data class TagAttributesDto(val group: String) : AttributesDto()
typealias LocalizedString = @Serializable(LocalizedStringSerializer::class) 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()
object LocalizedStringSerializer : KSerializer<Map<String, String>> {
override val descriptor = buildClassSerialDescriptor("LocalizedString")
override fun deserialize(decoder: Decoder): Map<String, String> {
require(decoder is JsonDecoder)
return (decoder.decodeJsonElement() as? JsonObject)
?.mapValues { it.value.jsonPrimitive.contentOrNull ?: "" }
.orEmpty()
}
override fun serialize(encoder: Encoder, value: Map<String, String>) {
encoder.encodeSerializableValue(serializer(), value)
}
}

View File

@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import kotlinx.serialization.Serializable
@Serializable
data class PaginatedResponseDto<T : EntityDto>(
val result: String,
val response: String = "",
val data: List<T> = emptyList(),
val limit: Int = 0,
val offset: Int = 0,
val total: Int = 0
)
@Serializable
data class ResponseDto<T : EntityDto>(
val result: String,
val response: String = "",
val data: T? = null
)

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName(MDConstants.scanlationGroup)
data class ScanlationGroupDto(override val attributes: ScanlationGroupAttributes? = null) : EntityDto()
@Serializable
data class ScanlationGroupAttributes(val name: String) : AttributesDto()

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.all.mangadex.dto
import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName(MDConstants.user)
data class UserDto(override val attributes: UserAttributes? = null) : EntityDto()
@Serializable
data class UserAttributes(val username: String) : AttributesDto()