diff --git a/src/all/mangadex/build.gradle b/src/all/mangadex/build.gradle index 5c27d365b..78415adf3 100644 --- a/src/all/mangadex/build.gradle +++ b/src/all/mangadex/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'MangaDex' pkgNameSuffix = 'all.mangadex' extClass = '.MangaDexFactory' - extVersionCode = 174 + extVersionCode = 175 isNsfw = true } diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt index 25fd4861c..b307b01b2 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt @@ -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" diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt index 393f2dbe4..82ba9eedd 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt @@ -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() + .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() .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() + .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 { - 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().data.relationships - .find { it.type == MDConstants.manga }!!.id + response.parseAs().data!!.relationships + .filterIsInstance() + .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() - val listDtoFiltered = listDto.data.relationships.filter { it.type != "Manga" } + val listDtoFiltered = listDto.data!!.relationships.filterIsInstance() 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() + .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() 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 { - 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() } @@ -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): Map? { 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().data + client.newCall(GET(apiUrl, headers)).execute().parseAs().data } val covers = result.getOrNull() ?: return null return covers - .groupBy { it.relationships.find { r -> r.type == MDConstants.manga }!!.id } + .groupBy { + it.relationships.filterIsInstance() + .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) diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt index 3ad885be1..e0613bc29 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt @@ -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) { diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt index 0fc169e05..bde5cec80 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt @@ -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() + .mapNotNull { it.attributes?.name } .distinct() val artists = mangaDataDto.relationships - .filter { it.type.equals(MDConstants.artist, true) } - .mapNotNull { it.attributes!!.name } + .filterIsInstance() + .mapNotNull { it.attributes?.name } .distinct() val coverFileName = firstVolumeCover ?: mangaDataDto.relationships - .firstOrNull { it.type.equals(MDConstants.coverArt, true) } + .filterIsInstance() + .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() .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() + .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) diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt index 7f25aed19..83ef53c2a 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt @@ -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 = mapOf( - "safe" to contentRatingSafe, - "suggestive" to contentRatingSuggestive, - "erotica" to contentRatingErotica, - "pornographic" to contentRatingPornographic + private val contentRatingMap: Map = 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" diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/AuthorDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/AuthorDto.kt new file mode 100644 index 000000000..b4de9bb70 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/AuthorDto.kt @@ -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() diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ChapterDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ChapterDto.kt index 7a203ac97..9e9adeac5 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ChapterDto.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ChapterDto.kt @@ -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, -) +typealias ChapterListDto = PaginatedResponseDto + +typealias ChapterDto = ResponseDto @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, -) +@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() diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverArtDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverArtDto.kt new file mode 100644 index 000000000..75d479a09 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverArtDto.kt @@ -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 + +@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() diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverDto.kt deleted file mode 100644 index 9f8c714a5..000000000 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/CoverDto.kt +++ /dev/null @@ -1,22 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.mangadex.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class CoverListDto( - val data: List = emptyList() -) - -@Serializable -data class CoverDto( - val id: String, - val attributes: CoverAttributesDto? = null, - val relationships: List = emptyList() -) - -@Serializable -data class CoverAttributesDto( - val name: String? = null, - val fileName: String? = null, - val locale: String? = null -) diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/EntityDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/EntityDto.kt new file mode 100644 index 000000000..b2dc618ed --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/EntityDto.kt @@ -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 = emptyList() + abstract val attributes: AttributesDto? +} + +@Serializable +abstract class AttributesDto diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ListDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ListDto.kt index c7b71e69f..bf6513235 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ListDto.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ListDto.kt @@ -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 @Serializable -data class ListDataDto( - val id: String, - val type: String, - val attributes: ListAttributesDto, - val relationships: List, -) +@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() diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt index 78ed3a718..180ad7533 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/MangaDto.kt @@ -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 + +typealias MangaDto = ResponseDto @Serializable -data class MangaListDto( - val limit: Int, - val offset: Int, - val total: Int, - val data: List, -) - -@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, -) +@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, + 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, -) +) : 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 +@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 /** * 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> { + override val descriptor = buildClassSerialDescriptor("LocalizedString") + + override fun deserialize(decoder: Decoder): Map { + require(decoder is JsonDecoder) + + return (decoder.decodeJsonElement() as? JsonObject) + ?.mapValues { it.value.jsonPrimitive.contentOrNull ?: "" } + .orEmpty() + } + + override fun serialize(encoder: Encoder, value: Map) { + encoder.encodeSerializableValue(serializer(), value) + } } diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ResponseDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ResponseDto.kt new file mode 100644 index 000000000..86239546d --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ResponseDto.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.extension.all.mangadex.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class PaginatedResponseDto( + val result: String, + val response: String = "", + val data: List = emptyList(), + val limit: Int = 0, + val offset: Int = 0, + val total: Int = 0 +) + +@Serializable +data class ResponseDto( + val result: String, + val response: String = "", + val data: T? = null +) diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ScanlationGroupDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ScanlationGroupDto.kt new file mode 100644 index 000000000..5966de082 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/ScanlationGroupDto.kt @@ -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() diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/UserDto.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/UserDto.kt new file mode 100644 index 000000000..7e4207883 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/dto/UserDto.kt @@ -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()