diff --git a/src/all/mangadex/build.gradle b/src/all/mangadex/build.gradle index 6eb2f159c..cb3044b17 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 = 158 + extVersionCode = 159 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 4549a7a99..7bd954526 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 @@ -46,7 +46,8 @@ object MDConstants { return "${coverQualityPref}_$dexLang" } - fun getCoverQualityPreferenceEntries() = arrayOf("Original", "Medium", "Low") + fun getCoverQualityPreferenceEntries(intl: MangaDexIntl) = + arrayOf(intl.coverQualityOriginal, intl.coverQualityMedium, intl.coverQualityLow) fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg") @@ -76,10 +77,11 @@ object MDConstants { } private const val originalLanguagePref = "originalLanguage" - const val originalLanguagePrefValJapanese = "ja" - const val originalLanguagePrefValChinese = "zh" + const val originalLanguagePrefValJapanese = MangaDexIntl.JAPANESE + const val originalLanguagePrefValChinese = MangaDexIntl.CHINESE const val originalLanguagePrefValChineseHk = "zh-hk" - const val originalLanguagePrefValKorean = "ko" + const val originalLanguagePrefValKorean = MangaDexIntl.KOREAN + val originalLanguagePrefDefaults = emptySet() fun getOriginalLanguagePrefKey(dexLang: String): String { return "${originalLanguagePref}_$dexLang" @@ -100,4 +102,10 @@ object MDConstants { fun getBlockedUploaderPrefKey(dexLang: String): String { return "${blockedUploaderPref}_$dexLang" } + + const val tagGroupContent = "content" + const val tagGroupFormat = "format" + const val tagGroupGenre = "genre" + const val tagGroupTheme = "theme" + val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme) } 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 266ae29bd..4d0679bf4 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 @@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterListDto import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDto 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.source.ConfigurableSource @@ -28,6 +29,7 @@ import kotlinx.serialization.SerializationException import kotlinx.serialization.decodeFromString import okhttp3.CacheControl import okhttp3.Headers +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Request @@ -37,10 +39,11 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Date -abstract class MangaDex(override val lang: String, val dexLang: String) : +abstract class MangaDex(final override val lang: String, private val dexLang: String) : ConfigurableSource, HttpSource() { - override val name = "MangaDex" + + override val name = MangaDexIntl.MANGADEX_NAME override val baseUrl = "https://mangadex.org" override val supportsLatest = true @@ -49,9 +52,9 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : Injekt.get().getSharedPreferences("source_$id", 0x0000) } - private val helper = MangaDexHelper() + private val helper = MangaDexHelper(lang) - override fun headersBuilder() = Headers.Builder() + final override fun headersBuilder() = Headers.Builder() .add("Referer", "$baseUrl/") .add("User-Agent", "Tachiyomi " + System.getProperty("http.agent")) @@ -70,37 +73,44 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : addQueryParameter("limit", MDConstants.mangaLimit.toString()) addQueryParameter("offset", helper.getMangaListOffset(page)) addQueryParameter("includes[]", MDConstants.coverArt) - preferences.getStringSet( - MDConstants.getContentRatingPrefKey(dexLang), - MDConstants.contentRatingPrefDefaults - )?.forEach { addQueryParameter("contentRating[]", it) } - preferences.getStringSet( + + addQueryParameter( + "contentRating[]", + preferences.getStringSet( + MDConstants.getContentRatingPrefKey(dexLang), + MDConstants.contentRatingPrefDefaults + ) + ) + + val originalLanguages = preferences.getStringSet( MDConstants.getOriginalLanguagePrefKey(dexLang), - setOf() - )?.forEach { - addQueryParameter("originalLanguage[]", it) - // dex has zh and zh-hk for chinese manhua - if (it == MDConstants.originalLanguagePrefValChinese) { - addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk) - } + MDConstants.originalLanguagePrefDefaults + ) + + addQueryParameter("originalLanguage[]", originalLanguages) + + // Dex has zh and zh-hk for Chinese manhua + if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) { + addQueryParameter( + "originalLanguage[]", + MDConstants.originalLanguagePrefValChineseHk + ) } - }.build().toUrl().toString() + } + return GET( - url = url, + url = url.build().toString(), headers = headers, cache = CacheControl.FORCE_NETWORK ) } override fun popularMangaParse(response: Response): MangasPage { - if (response.isSuccessful.not()) { - throw Exception("HTTP ${response.code}") - } - if (response.code == 204) { return MangasPage(emptyList(), false) } - val mangaListDto = helper.json.decodeFromString(response.body!!.string()) + + val mangaListDto = response.parseAs() val hasMoreResults = mangaListDto.limit + mangaListDto.offset < mangaListDto.total val coverSuffix = preferences.getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "") @@ -117,28 +127,34 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : // LATEST section API can't sort by date yet so not implemented override fun latestUpdatesParse(response: Response): MangasPage { - val chapterListDto = helper.json.decodeFromString(response.body!!.string()) + val chapterListDto = response.parseAs() val hasMoreResults = chapterListDto.limit + chapterListDto.offset < chapterListDto.total - val mangaIds = chapterListDto.data.map { it.relationships }.flatten() - .filter { it.type == MDConstants.manga }.map { it.id }.distinct() + val mangaIds = chapterListDto.data + .flatMap { it.relationships } + .filter { it.type == MDConstants.manga } + .map { it.id } + .distinct() + .toSet() val mangaUrl = MDConstants.apiMangaUrl.toHttpUrlOrNull()!!.newBuilder().apply { addQueryParameter("includes[]", MDConstants.coverArt) addQueryParameter("limit", mangaIds.size.toString()) - preferences.getStringSet( - MDConstants.getContentRatingPrefKey(dexLang), - MDConstants.contentRatingPrefDefaults - )?.forEach { addQueryParameter("contentRating[]", it) } + addQueryParameter( + "contentRating[]", + preferences.getStringSet( + MDConstants.getContentRatingPrefKey(dexLang), + MDConstants.contentRatingPrefDefaults + ) + ) - mangaIds.forEach { id -> - addQueryParameter("ids[]", id) - } - }.build().toString() + addQueryParameter("ids[]", mangaIds) + } - val mangaResponse = client.newCall(GET(mangaUrl, headers, CacheControl.FORCE_NETWORK)).execute() - val mangaListDto = helper.json.decodeFromString(mangaResponse.body!!.string()) + val mangaRequest = GET(mangaUrl.build().toString(), headers, CacheControl.FORCE_NETWORK) + val mangaResponse = client.newCall(mangaRequest).execute() + val mangaListDto = mangaResponse.parseAs() val mangaDtoMap = mangaListDto.data.associateBy({ it.id }, { it }) @@ -154,6 +170,7 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : return MangasPage(mangaList, hasMoreResults) } + override fun latestUpdatesRequest(page: Int): Request { val url = MDConstants.apiChapterUrl.toHttpUrlOrNull()!!.newBuilder().apply { addQueryParameter("offset", helper.getLatestChapterOffset(page)) @@ -161,32 +178,52 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : addQueryParameter("translatedLanguage[]", dexLang) addQueryParameter("order[publishAt]", "desc") addQueryParameter("includeFutureUpdates", "0") - preferences.getStringSet( + + val originalLanguages = preferences.getStringSet( MDConstants.getOriginalLanguagePrefKey(dexLang), - setOf() - )?.forEach { - addQueryParameter("originalLanguage[]", it) - // dex has zh and zh-hk for chinese manhua - if (it == MDConstants.originalLanguagePrefValChinese) { - addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk) - } + MDConstants.originalLanguagePrefDefaults + ) + + addQueryParameter("originalLanguage[]", originalLanguages) + + // Dex has zh and zh-hk for Chinese manhua + if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) { + addQueryParameter( + "originalLanguage[]", + MDConstants.originalLanguagePrefValChineseHk + ) } - preferences.getStringSet( - MDConstants.getContentRatingPrefKey(dexLang), - MDConstants.contentRatingPrefDefaults - )?.forEach { addQueryParameter("contentRating[]", it) } - MDConstants.defaultBlockedGroups.forEach { - addQueryParameter("excludedGroups[]", it) - } - preferences.getString( - MDConstants.getBlockedGroupsPrefKey(dexLang), "" - )?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedGroups[]", it.trim()) } - preferences.getString( - MDConstants.getBlockedUploaderPrefKey(dexLang), - "" - )?.split(", ")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedUploaders[]", it.trim()) } - }.build().toString() - return GET(url, headers, CacheControl.FORCE_NETWORK) + + addQueryParameter( + "contentRating[]", + preferences.getStringSet( + MDConstants.getContentRatingPrefKey(dexLang), + MDConstants.contentRatingPrefDefaults + ) + ) + + val excludedGroups = MDConstants.defaultBlockedGroups + + preferences.getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "") + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotEmpty) + ?.sorted() + .orEmpty() + + addQueryParameter("excludedGroups[]", excludedGroups) + + val excludedUploaders = preferences + .getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "") + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotEmpty) + ?.sorted() + ?.toSet() + + addQueryParameter("excludedUploaders[]", excludedUploaders) + } + + return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK) } // SEARCH section @@ -197,14 +234,17 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : return getMangaIdFromChapterId(query.removePrefix(MDConstants.prefixChSearch)).flatMap { manga_id -> super.fetchSearchManga(page, MDConstants.prefixIdSearch + manga_id, filters) } + query.startsWith(MDConstants.prefixUsrSearch) -> return client.newCall(searchMangaUploaderRequest(page, 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)) .asObservableSuccess() .map { searchMangaListRequest(it, page) } + else -> return super.fetchSearchManga(page, query, filters) } @@ -215,20 +255,18 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : .asObservableSuccess() .map { response -> if (response.isSuccessful.not()) { - throw Exception("Unable to process Chapter request. HTTP code: ${response.code}") + throw Exception(helper.intl.unableToProcessChapterRequest(response.code)) } - helper.json.decodeFromString(response.body!!.string()).data.relationships - .find { - it.type == MDConstants.manga - }!!.id + response.parseAs().data.relationships + .find { it.type == MDConstants.manga }!!.id } } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder().apply { addQueryParameter("limit", MDConstants.mangaLimit.toString()) - addQueryParameter("offset", (helper.getMangaListOffset(page))) + addQueryParameter("offset", helper.getMangaListOffset(page)) addQueryParameter("includes[]", MDConstants.coverArt) } @@ -241,24 +279,26 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : addQueryParameter("contentRating[]", "suggestive") addQueryParameter("contentRating[]", "erotica") addQueryParameter("contentRating[]", "pornographic") - }.build().toString() + } - return GET(url, headers, CacheControl.FORCE_NETWORK) + return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK) } + query.startsWith(MDConstants.prefixGrpSearch) -> { val groupID = query.removePrefix(MDConstants.prefixGrpSearch) if (!helper.containsUuid(groupID)) { - throw Exception("Not a valid group ID") + throw Exception(helper.intl.invalidGroupId) } tempUrl.apply { addQueryParameter("group", groupID) } } + query.startsWith(MDConstants.prefixAuthSearch) -> { val authorID = query.removePrefix(MDConstants.prefixAuthSearch) if (!helper.containsUuid(authorID)) { - throw Exception("Not a valid author ID") + throw Exception(helper.intl.invalidAuthorId) } tempUrl.apply { @@ -266,6 +306,7 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : addQueryParameter("artists[]", authorID) } } + else -> { tempUrl.apply { val actualQuery = query.replace(MDConstants.whitespaceRegex, " ") @@ -284,16 +325,14 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) private fun searchMangaListRequest(response: Response, page: Int): MangasPage { - if (response.isSuccessful.not()) { - throw Exception("HTTP ${response.code}") - } - - val listDto = helper.json.decodeFromString(response.body!!.string()) + val listDto = response.parseAs() val listDtoFiltered = listDto.data.relationships.filter { it.type != "Manga" } val amount = listDtoFiltered.count() + if (amount < 1) { - throw Exception("No Manga in List") + throw Exception(helper.intl.noSeriesInList) } + val minIndex = (page - 1) * MDConstants.mangaLimit val url = MDConstants.apiMangaUrl.toHttpUrl().newBuilder().apply { @@ -301,23 +340,29 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : addQueryParameter("offset", "0") addQueryParameter("includes[]", MDConstants.coverArt) } - listDtoFiltered.forEachIndexed() { index, relationshipDto -> - if (index >= minIndex && index < (minIndex + MDConstants.mangaLimit)) { - url.addQueryParameter("ids[]", relationshipDto.id) - } - } - val request = client.newCall(GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)) - val mangaList = searchMangaListParse(request.execute()) - return MangasPage(mangaList, amount.toFloat() / MDConstants.mangaLimit - (page.toFloat() - 1) > 1) + val ids = listDtoFiltered + .filterIndexed { i, _ -> i >= minIndex && i < (minIndex + MDConstants.mangaLimit) } + .map(RelationshipDto::id) + .toSet() + + url.addQueryParameter("ids[]", ids) + + val mangaRequest = GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK) + val mangaResponse = client.newCall(mangaRequest).execute() + val mangaList = searchMangaListParse(mangaResponse) + + val hasNextPage = amount.toFloat() / MDConstants.mangaLimit - (page.toFloat() - 1) > 1 + + return MangasPage(mangaList, hasNextPage) } private fun searchMangaListParse(response: Response): List { if (response.isSuccessful.not()) { - throw Exception("HTTP ${response.code}") + throw Exception("HTTP error ${response.code}") } - val mangaListDto = helper.json.decodeFromString(response.body!!.string()) + val mangaListDto = response.parseAs() val coverSuffix = preferences.getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "") @@ -339,32 +384,52 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : addQueryParameter("order[publishAt]", "desc") addQueryParameter("includeFutureUpdates", "0") addQueryParameter("uploader", uploader) - preferences.getStringSet( + + val originalLanguages = preferences.getStringSet( MDConstants.getOriginalLanguagePrefKey(dexLang), - setOf() - )?.forEach { - addQueryParameter("originalLanguage[]", it) - // dex has zh and zh-hk for chinese manhua - if (it == MDConstants.originalLanguagePrefValChinese) { - addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk) - } + MDConstants.originalLanguagePrefDefaults + ) + + addQueryParameter("originalLanguage[]", originalLanguages) + + // Dex has zh and zh-hk for Chinese manhua + if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) { + addQueryParameter( + "originalLanguage[]", + MDConstants.originalLanguagePrefValChineseHk + ) } - preferences.getStringSet( - MDConstants.getContentRatingPrefKey(dexLang), - MDConstants.contentRatingPrefDefaults - )?.forEach { addQueryParameter("contentRating[]", it) } - MDConstants.defaultBlockedGroups.forEach { - addQueryParameter("excludedGroups[]", it) - } - preferences.getString( - MDConstants.getBlockedGroupsPrefKey(dexLang), "" - )?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedGroups[]", it.trim()) } - preferences.getString( - MDConstants.getBlockedUploaderPrefKey(dexLang), - "" - )?.split(", ")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedUploaders[]", it.trim()) } - }.build().toString() - return GET(url, headers, CacheControl.FORCE_NETWORK) + + addQueryParameter( + "contentRating[]", + preferences.getStringSet( + MDConstants.getContentRatingPrefKey(dexLang), + MDConstants.contentRatingPrefDefaults + ) + ) + + val excludedGroups = MDConstants.defaultBlockedGroups + + preferences.getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "") + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotEmpty) + ?.sorted() + .orEmpty() + + addQueryParameter("excludedGroups[]", excludedGroups) + + val excludedUploaders = preferences + .getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "") + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotEmpty) + ?.sorted() + ?.toSet() + + addQueryParameter("excludedUploaders[]", excludedUploaders) + } + + return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK) } // Manga Details section @@ -391,20 +456,28 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : */ private fun apiMangaDetailsRequest(manga: SManga): Request { if (!helper.containsUuid(manga.url.trim())) { - throw Exception("Migrate this manga from MangaDex to MangaDex to update it") + throw Exception(helper.intl.migrateWarning) } + val url = (MDConstants.apiUrl + manga.url).toHttpUrl().newBuilder().apply { addQueryParameter("includes[]", MDConstants.coverArt) addQueryParameter("includes[]", MDConstants.author) addQueryParameter("includes[]", MDConstants.artist) - }.build().toString() - return GET(url, headers, CacheControl.FORCE_NETWORK) + } + + return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK) } override fun mangaDetailsParse(response: Response): SManga { - val manga = helper.json.decodeFromString(response.body!!.string()) + val manga = response.parseAs() val coverSuffix = preferences.getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "") - return helper.createManga(manga.data, fetchSimpleChapterList(manga, dexLang), dexLang, coverSuffix) + + return helper.createManga( + manga.data, + fetchSimpleChapterList(manga, dexLang), + dexLang, + coverSuffix + ) } /** @@ -418,13 +491,18 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : val url = "${MDConstants.apiMangaUrl}/${manga.data.id}/aggregate?translatedLanguage[]=$langCode" val response = client.newCall(GET(url, headers)).execute() val chapters: AggregateDto + try { - chapters = helper.json.decodeFromString(response.body!!.string()) + chapters = response.parseAs() } catch (e: SerializationException) { return emptyList() } + if (chapters.volumes.isNullOrEmpty()) return emptyList() - return chapters.volumes.values.flatMap { it.chapters.values }.map { it.chapter } + + return chapters.volumes.values + .flatMap { it.chapters.values } + .map { it.chapter } } // Chapter list section @@ -433,8 +511,9 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : */ override fun chapterListRequest(manga: SManga): Request { if (!helper.containsUuid(manga.url)) { - throw Exception("Migrate this manga from MangaDex to MangaDex to update it") + throw Exception(helper.intl.migrateWarning) } + return actualChapterListRequest(helper.getUUIDFromUrl(manga.url), 0) } @@ -447,31 +526,44 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : addQueryParameter("contentRating[]", "suggestive") addQueryParameter("contentRating[]", "erotica") addQueryParameter("contentRating[]", "pornographic") - preferences.getString( - MDConstants.getBlockedGroupsPrefKey(dexLang), "" - )?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedGroups[]", it.trim()) } - preferences.getString( - MDConstants.getBlockedUploaderPrefKey(dexLang), - "" - )?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedUploaders[]", it.trim()) } - }.build().toString() - return GET(url, headers = headers, cache = CacheControl.FORCE_NETWORK) - } - override fun chapterListParse(response: Response): List { - if (response.isSuccessful.not()) { - throw Exception("HTTP ${response.code}") + + val excludedGroups = preferences + .getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "") + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotEmpty) + ?.sorted() + ?.toSet() + + addQueryParameter("excludedGroups[]", excludedGroups) + + val excludedUploaders = preferences + .getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "") + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotEmpty) + ?.sorted() + ?.toSet() + + addQueryParameter("excludedUploaders[]", excludedUploaders) } + + return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK) + } + + override fun chapterListParse(response: Response): List { if (response.code == 204) { return emptyList() } + try { - val chapterListResponse = helper.json.decodeFromString(response.body!!.string()) + val chapterListResponse = response.parseAs() val chapterListResults = chapterListResponse.data.toMutableList() - val mangaId = - response.request.url.toString().substringBefore("/feed") - .substringAfter("${MDConstants.apiMangaUrl}/") + val mangaId = response.request.url.toString() + .substringBefore("/feed") + .substringAfter("${MDConstants.apiMangaUrl}/") val limit = chapterListResponse.limit @@ -479,31 +571,32 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : var hasMoreResults = (limit + offset) < chapterListResponse.total - // max results that can be returned is 500 so need to make more api calls if limit+offset > total chapters + // Max results that can be returned is 500 so need to make more API + // calls if limit + offset > total chapters while (hasMoreResults) { offset += limit - val newResponse = - client.newCall(actualChapterListRequest(mangaId, offset)).execute() - val newChapterList = helper.json.decodeFromString(newResponse.body!!.string()) + val newRequest = actualChapterListRequest(mangaId, offset) + val newResponse = client.newCall(newRequest).execute() + val newChapterList = newResponse.parseAs() chapterListResults.addAll(newChapterList.data) 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(it) } + .filter { it.date_upload <= now } } catch (e: Exception) { Log.e("MangaDex", "error parsing chapter list", e) - throw(e) + throw e } } override fun pageListRequest(chapter: SChapter): Request { if (!helper.containsUuid(chapter.url)) { - throw Exception("Migrate this manga from MangaDex to MangaDex to update it") + throw Exception(helper.intl.migrateWarning) } + val chapterId = chapter.url.substringAfter("/chapter/") val usingStandardHTTPS = preferences.getBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), false) @@ -518,7 +611,7 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : override fun pageListParse(response: Response): List { val atHomeRequestUrl = response.request.url - val atHomeDto = helper.json.decodeFromString(response.body!!.string()) + val atHomeDto = response.parseAs() val host = atHomeDto.baseUrl val usingDataSaver = preferences.getBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), false) @@ -548,8 +641,8 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : override fun setupPreferenceScreen(screen: PreferenceScreen) { val coverQualityPref = ListPreference(screen.context).apply { key = MDConstants.getCoverQualityPreferenceKey(dexLang) - title = "Manga Cover Quality" - entries = MDConstants.getCoverQualityPreferenceEntries() + title = helper.intl.coverQuality + entries = MDConstants.getCoverQualityPreferenceEntries(helper.intl) entryValues = MDConstants.getCoverQualityPreferenceEntryValues() setDefaultValue(MDConstants.getCoverQualityPreferenceDefaultValue()) summary = "%s" @@ -558,18 +651,22 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : val selected = newValue as String val index = findIndexOfValue(selected) val entry = entryValues[index] as String - preferences.edit().putString(MDConstants.getCoverQualityPreferenceKey(dexLang), entry).commit() + + preferences.edit() + .putString(MDConstants.getCoverQualityPreferenceKey(dexLang), entry) + .commit() } } val dataSaverPref = SwitchPreferenceCompat(screen.context).apply { key = MDConstants.getDataSaverPreferenceKey(dexLang) - title = "Data saver" - summary = "Enables smaller, more compressed images" + title = helper.intl.dataSaver + summary = helper.intl.dataSaverSummary setDefaultValue(false) setOnPreferenceChangeListener { _, newValue -> val checkValue = newValue as Boolean + preferences.edit() .putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue) .commit() @@ -578,13 +675,13 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : val standardHttpsPortPref = SwitchPreferenceCompat(screen.context).apply { key = MDConstants.getStandardHttpsPreferenceKey(dexLang) - title = "Use HTTPS port 443 only" - summary = - "Enable to only request image servers that use port 443. This allows users with stricter firewall restrictions to access MangaDex images" + title = helper.intl.standardHttpsPort + summary = helper.intl.standardHttpsPortSummary setDefaultValue(false) setOnPreferenceChangeListener { _, newValue -> val checkValue = newValue as Boolean + preferences.edit() .putBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), checkValue) .commit() @@ -593,9 +690,14 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : val contentRatingPref = MultiSelectListPreference(screen.context).apply { key = MDConstants.getContentRatingPrefKey(dexLang) - title = "Default content rating" - summary = "Show content with the selected ratings by default" - entries = arrayOf("Safe", "Suggestive", "Erotica", "Pornographic") + title = helper.intl.standardContentRating + summary = helper.intl.standardContentRatingSummary + entries = arrayOf( + helper.intl.contentRatingSafe, + helper.intl.contentRatingSuggestive, + helper.intl.contentRatingErotica, + helper.intl.contentRatingPornographic + ) entryValues = arrayOf( MDConstants.contentRatingPrefValSafe, MDConstants.contentRatingPrefValSuggestive, @@ -603,8 +705,10 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : MDConstants.contentRatingPrefValPornographic ) setDefaultValue(MDConstants.contentRatingPrefDefaults) + setOnPreferenceChangeListener { _, newValue -> val checkValue = newValue as Set + preferences.edit() .putStringSet(MDConstants.getContentRatingPrefKey(dexLang), checkValue) .commit() @@ -613,17 +717,23 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : val originalLanguagePref = MultiSelectListPreference(screen.context).apply { key = MDConstants.getOriginalLanguagePrefKey(dexLang) - title = "Filter original languages" - summary = "Only show content that was originally published in the selected languages in both latest and browse" - entries = arrayOf("Japanese", "Chinese", "Korean") + title = helper.intl.filterOriginalLanguages + summary = helper.intl.filterOriginalLanguagesSummary + entries = arrayOf( + helper.intl.languageDisplayName(MangaDexIntl.JAPANESE), + helper.intl.languageDisplayName(MangaDexIntl.CHINESE), + helper.intl.languageDisplayName(MangaDexIntl.KOREAN) + ) entryValues = arrayOf( MDConstants.originalLanguagePrefValJapanese, MDConstants.originalLanguagePrefValChinese, MDConstants.originalLanguagePrefValKorean ) setDefaultValue(setOf()) + setOnPreferenceChangeListener { _, newValue -> val checkValue = newValue as Set + preferences.edit() .putStringSet(MDConstants.getOriginalLanguagePrefKey(dexLang), checkValue) .commit() @@ -632,15 +742,15 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : val blockedGroupsPref = EditTextPreference(screen.context).apply { key = MDConstants.getBlockedGroupsPrefKey(dexLang) - title = "Block Groups by UUID" - summary = "Chapters from blocked groups will not show up in Latest or Manga feed.\n" + - "Enter as a Comma-separated list of group UUIDs" + title = helper.intl.blockGroupByUuid + summary = helper.intl.blockGroupByUuidSummary + setOnPreferenceChangeListener { _, newValue -> val groupsBlocked = newValue.toString() .split(",") .map { it.trim() } .filter { helper.containsUuid(it) } - .joinToString(separator = ", ") + .joinToString(", ") preferences.edit() .putString(MDConstants.getBlockedGroupsPrefKey(dexLang), groupsBlocked) @@ -650,15 +760,15 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : val blockedUploaderPref = EditTextPreference(screen.context).apply { key = MDConstants.getBlockedUploaderPrefKey(dexLang) - title = "Block Uploader by UUID" - summary = "Chapters from blocked users will not show up in Latest or Manga feed.\n" + - "Enter as a Comma-separated list of uploader UUIDs" + title = helper.intl.blockUploaderByUuid + summary = helper.intl.blockUploaderByUuidSummary + setOnPreferenceChangeListener { _, newValue -> val uploaderBlocked = newValue.toString() .split(",") .map { it.trim() } .filter { helper.containsUuid(it) } - .joinToString(separator = ", ") + .joinToString(", ") preferences.edit() .putString(MDConstants.getBlockedUploaderPrefKey(dexLang), uploaderBlocked) @@ -676,5 +786,13 @@ abstract class MangaDex(override val lang: String, val dexLang: String) : } override fun getFilterList(): FilterList = - helper.mdFilters.getMDFilterList(preferences, dexLang) + helper.mdFilters.getMDFilterList(preferences, dexLang, helper.intl) + + private fun HttpUrl.Builder.addQueryParameter(name: String, value: Set?): HttpUrl.Builder { + return apply { value?.forEach { addQueryParameter(name, it) } } + } + + private inline fun Response.parseAs(): T = use { + helper.json.decodeFromString(body?.string().orEmpty()) + } } 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 f3d6afcd1..9dc6b0ae5 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 @@ -4,301 +4,376 @@ import android.content.SharedPreferences import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import okhttp3.HttpUrl -import java.util.Locale class MangaDexFilters { - internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String): FilterList { - + internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): FilterList { return FilterList( - OriginalLanguageList(getOriginalLanguage(preferences, dexLang)), - ContentRatingList(getContentRating(preferences, dexLang)), - DemographicList(getDemographics()), - StatusList(getStatus()), - SortFilter(sortableList.map { it.first }.toTypedArray()), - TagList(getTags()), - TagInclusionMode(), - TagExclusionMode(), - HasAvailableChaptersFilter(), + HasAvailableChaptersFilter(intl), + OriginalLanguageList(intl, getOriginalLanguage(preferences, dexLang, intl)), + ContentRatingList(intl, getContentRating(preferences, dexLang, intl)), + DemographicList(intl, getDemographics(intl)), + StatusList(intl, getStatus(intl)), + SortFilter(intl, getSortables(intl)), + TagsFilter(intl, getTagFilters(intl)), + TagList(intl.content, getContents(intl)), + TagList(intl.format, getFormats(intl)), + TagList(intl.genre, getGenres(intl)), + TagList(intl.theme, getThemes(intl)), ) } - private fun getContentRating(preferences: SharedPreferences, dexLang: String): List { + private interface UrlQueryFilter { + fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) + } + + private fun getContentRating(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): List { val contentRatings = preferences.getStringSet( MDConstants.getContentRatingPrefKey(dexLang), MDConstants.contentRatingPrefDefaults ) return listOf( - ContentRating("Safe").apply { + ContentRating(intl.contentRatingSafe, MDConstants.contentRatingPrefValSafe).apply { state = contentRatings ?.contains(MDConstants.contentRatingPrefValSafe) ?: true }, - ContentRating("Suggestive").apply { + ContentRating(intl.contentRatingSuggestive, MDConstants.contentRatingPrefValSuggestive).apply { state = contentRatings ?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true }, - ContentRating("Erotica").apply { + ContentRating(intl.contentRatingErotica, MDConstants.contentRatingPrefValErotica).apply { state = contentRatings ?.contains(MDConstants.contentRatingPrefValErotica) ?: false }, - ContentRating("Pornographic").apply { + ContentRating(intl.contentRatingPornographic, MDConstants.contentRatingPrefValPornographic).apply { state = contentRatings ?.contains(MDConstants.contentRatingPrefValPornographic) ?: false }, ) } - private class Demographic(name: String) : Filter.CheckBox(name) - private class DemographicList(demographics: List) : - Filter.Group("Publication Demographic", demographics) + private class Demographic(name: String, val value: String) : Filter.CheckBox(name) + private class DemographicList(intl: MangaDexIntl, demographics: List) : + Filter.Group(intl.publicationDemographic, demographics), + UrlQueryFilter { - private fun getDemographics() = listOf( - Demographic("None"), - Demographic("Shounen"), - Demographic("Shoujo"), - Demographic("Seinen"), - Demographic("Josei") + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + state.forEach { demographic -> + if (demographic.state) { + url.addQueryParameter("publicationDemographic[]", demographic.value) + } + } + } + } + + 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") ) - private class Status(name: String) : Filter.CheckBox(name) - private class StatusList(status: List) : - Filter.Group("Status", status) + private class Status(name: String, val value: String) : Filter.CheckBox(name) + private class StatusList(intl: MangaDexIntl, status: List) : + Filter.Group(intl.status, status), + UrlQueryFilter { - private fun getStatus() = listOf( - Status("Ongoing"), - Status("Completed"), - Status("Hiatus"), - Status("Cancelled"), + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + state.forEach { status -> + if (status.state) { + url.addQueryParameter("status[]", status.value) + } + } + } + } + + private fun getStatus(intl: MangaDexIntl) = listOf( + Status(intl.statusOngoing, "ongoing"), + Status(intl.statusCompleted, "completed"), + Status(intl.statusHiatus, "hiatus"), + Status(intl.statusCancelled, "cancelled"), ) - private class ContentRating(name: String) : Filter.CheckBox(name) - private class ContentRatingList(contentRating: List) : - Filter.Group("Content Rating", contentRating) + private class ContentRating(name: String, val value: String) : Filter.CheckBox(name) + private class ContentRatingList(intl: MangaDexIntl, contentRating: List) : + Filter.Group(intl.contentRating, contentRating), + UrlQueryFilter { + + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + state.forEach { rating -> + if (rating.state) { + url.addQueryParameter("contentRating[]", rating.value) + } + } + } + } private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name) - private class OriginalLanguageList(originalLanguage: List) : - Filter.Group("Original language", originalLanguage) + private class OriginalLanguageList(intl: MangaDexIntl, originalLanguage: List) : + Filter.Group(intl.originalLanguage, originalLanguage), + UrlQueryFilter { - private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String): List { + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + state.forEach { lang -> + if (lang.state) { + // dex has zh and zh-hk for chinese manhua + if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) { + url.addQueryParameter( + "originalLanguage[]", + MDConstants.originalLanguagePrefValChineseHk + ) + } + + url.addQueryParameter("originalLanguage[]", lang.isoCode) + } + } + } + } + + private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): List { val originalLanguages = preferences.getStringSet( MDConstants.getOriginalLanguagePrefKey(dexLang), setOf() - ) + )!! + return listOf( - OriginalLanguage("Japanese (Manga)", "ja").apply { - state = originalLanguages - ?.contains(MDConstants.originalLanguagePrefValJapanese) ?: false - }, - OriginalLanguage("Chinese (Manhua)", "zh").apply { - state = originalLanguages - ?.contains(MDConstants.originalLanguagePrefValChinese) ?: false - }, - OriginalLanguage("Korean (Manhwa)", "ko").apply { - state = originalLanguages - ?.contains(MDConstants.originalLanguagePrefValKorean) ?: false - }, + OriginalLanguage(intl.originalLanguageFilterJapanese, MDConstants.originalLanguagePrefValJapanese) + .apply { state = MDConstants.originalLanguagePrefValJapanese in originalLanguages }, + OriginalLanguage(intl.originalLanguageFilterChinese, MDConstants.originalLanguagePrefValChinese) + .apply { state = MDConstants.originalLanguagePrefValChinese in originalLanguages }, + OriginalLanguage(intl.originalLanguageFilterKorean, MDConstants.originalLanguagePrefValKorean) + .apply { state = MDConstants.originalLanguagePrefValKorean in originalLanguages }, ) } internal class Tag(val id: String, name: String) : Filter.TriState(name) - private class TagList(tags: List) : Filter.Group("Tags", tags) - // to get all tags from dex https://api.mangadex.org/manga/tag - internal fun getTags() = listOf( - Tag("391b0423-d847-456f-aff0-8b0cfc03066b", "Action"), - Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", "Adaptation"), - Tag("87cc87cd-a395-47af-b27a-93258283bbc6", "Adventure"), - Tag("e64f6742-c834-471d-8d72-dd51fc02b835", "Aliens"), - Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", "Animals"), - Tag("51d83883-4103-437c-b4b1-731cb73d786c", "Anthology"), - Tag("0a39b5a1-b235-4886-a747-1d05d216532d", "Award Winning"), - Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", "Boy's Love"), - Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"), - Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"), - Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"), - Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", "Crossdressing"), - Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", "Delinquents"), - Tag("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"), - Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", "Doujinshi"), - Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", "Drama"), - Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", "Fan Colored"), - Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", "Fantasy"), - Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", "4-Koma"), - Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", "Full Color"), - Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", "Genderswap"), - Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", "Ghosts"), - Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", "Girl's Love"), - Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", "Gore"), - Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Gyaru"), - Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", "Harem"), - Tag("33771934-028e-4cb3-8744-691e866a923e", "Historical"), - Tag("cdad7e68-1419-41dd-bdce-27753074a640", "Horror"), - Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", "Incest"), - Tag("ace04997-f6bd-436e-b261-779182193d3d", "Isekai"), - Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", "Loli"), - Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", "Long Strip"), - Tag("85daba54-a71c-4554-8a28-9901a8b0afad", "Mafia"), - Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", "Magic"), - Tag("81c836c9-914a-4eca-981a-560dad663e73", "Magical Girls"), - Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", "Martial Arts"), - Tag("50880a9d-5440-4732-9afb-8f457127e836", "Mecha"), - Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", "Medical"), - Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", "Military"), - Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", "Monster Girls"), - Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", "Monsters"), - Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", "Music"), - Tag("ee968100-4191-4968-93d3-f82d72be7e46", "Mystery"), - Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Ninja"), - Tag("92d6d951-ca5e-429c-ac78-451071cbf064", "Office Workers"), - Tag("320831a8-4026-470b-94f6-8353740e6f04", "Official Colored"), - Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", "Oneshot"), - Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", "Philosophical"), - Tag("df33b754-73a3-4c54-80e6-1a74a8058539", "Police"), - Tag("9467335a-1b83-4497-9231-765337a00b96", "Post-Apocalyptic"), - Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", "Psychological"), - Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", "Reincarnation"), - Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", "Reverse Harem"), - Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", "Romance"), - Tag("81183756-1453-4c81-aa9e-f6e1b63be016", "Samurai"), - Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", "School Life"), - Tag("256c8bd9-4904-4360-bf4f-508a76d67183", "Sci-Fi"), - Tag("97893a4c-12af-4dac-b6be-0dffb353568e", "Sexual Violence"), - Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", "Shota"), - Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", "Slice of Life"), - Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", "Sports"), - Tag("7064a261-a137-4d3a-8848-2d385de3a99c", "Superhero"), - Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", "Supernatural"), - Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", "Survival"), - Tag("07251805-a27e-4d59-b488-f0bfbec15168", "Thriller"), - Tag("292e862b-2d17-4062-90a2-0356caa4ae27", "Time Travel"), - Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", "Tragedy"), - Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", "Traditional Games"), - Tag("891cf039-b895-47f0-9229-bef4c96eccd4", "User Created"), - Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", "Vampires"), - Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", "Video Games"), - Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", "Villainess"), - Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", "Virtual Reality"), - Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", "Web Comic"), - Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", "Wuxia"), - Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", "Zombies") - ) + private class TagList(collection: String, tags: List) : + Filter.Group(collection, tags), + UrlQueryFilter { - private class TagInclusionMode : - Filter.Select("Included tags mode", arrayOf("And", "Or"), 0) - - private class TagExclusionMode : - Filter.Select("Excluded tags mode", arrayOf("And", "Or"), 1) - - val sortableList = listOf( - Pair("Alphabetic", "title"), - Pair("Chapter uploaded at", "latestUploadedChapter"), - Pair("Number of follows", "followedCount"), - Pair("Manga created at", "createdAt"), - Pair("Manga info updated at", "updatedAt"), - Pair("Relevant manga", "relevance"), - Pair("Year", "year") - ) - - class SortFilter(sortables: Array) : Filter.Sort("Sort", sortables, Selection(2, false)) - - private class HasAvailableChaptersFilter : Filter.CheckBox("Has available chapters") - - internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String { - url.apply { - // add filters - filters.forEach { filter -> - when (filter) { - is OriginalLanguageList -> { - filter.state.forEach { lang -> - if (lang.state) { - // dex has zh and zh-hk for chinese manhua - if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) { - addQueryParameter( - "originalLanguage[]", - MDConstants.originalLanguagePrefValChineseHk - ) - } - addQueryParameter( - "originalLanguage[]", - lang.isoCode - ) - } - } - } - is ContentRatingList -> { - filter.state.forEach { rating -> - if (rating.state) { - addQueryParameter( - "contentRating[]", - rating.name.toLowerCase(Locale.US) - ) - } - } - } - is DemographicList -> { - filter.state.forEach { demographic -> - if (demographic.state) { - addQueryParameter( - "publicationDemographic[]", - demographic.name.toLowerCase( - Locale.US - ) - ) - } - } - } - is StatusList -> { - filter.state.forEach { status -> - if (status.state) { - addQueryParameter( - "status[]", - status.name.toLowerCase( - Locale.US - ) - ) - } - } - } - is SortFilter -> { - if (filter.state != null) { - val query = sortableList[filter.state!!.index].second - val value = when (filter.state!!.ascending) { - true -> "asc" - false -> "desc" - } - addQueryParameter("order[$query]", value) - } - } - is TagList -> { - filter.state.forEach { tag -> - if (tag.isIncluded()) { - addQueryParameter("includedTags[]", tag.id) - } else if (tag.isExcluded()) { - addQueryParameter("excludedTags[]", tag.id) - } - } - } - is TagInclusionMode -> { - addQueryParameter( - "includedTagsMode", - filter.values[filter.state].toUpperCase(Locale.US) - ) - } - is TagExclusionMode -> { - addQueryParameter( - "excludedTagsMode", - filter.values[filter.state].toUpperCase(Locale.US) - ) - } - is HasAvailableChaptersFilter -> { - if (filter.state) { - addQueryParameter("hasAvailableChapters", "true") - addQueryParameter("availableTranslatedLanguage[]", dexLang) - } - } + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + state.forEach { tag -> + if (tag.isIncluded()) { + url.addQueryParameter("includedTags[]", tag.id) + } else if (tag.isExcluded()) { + url.addQueryParameter("excludedTags[]", tag.id) } } } + } + + internal fun getContents(intl: MangaDexIntl): List { + val tags = listOf( + Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", intl.contentGore), + Tag("97893a4c-12af-4dac-b6be-0dffb353568e", intl.contentSexualViolence), + ) + + return tags.sortIfTranslated(intl) + } + + internal fun getFormats(intl: MangaDexIntl): List { + val tags = listOf( + Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", intl.formatFourKoma), + Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", intl.formatAdaptation), + Tag("51d83883-4103-437c-b4b1-731cb73d786c", intl.formatAnthology), + Tag("0a39b5a1-b235-4886-a747-1d05d216532d", intl.formatAwardWinning), + Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", intl.formatDoujinshi), + Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", intl.formatFanColored), + Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", intl.formatFullColor), + Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", intl.formatLongStrip), + Tag("320831a8-4026-470b-94f6-8353740e6f04", intl.formatOfficialColored), + Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", intl.formatOneshot), + Tag("891cf039-b895-47f0-9229-bef4c96eccd4", intl.formatUserCreated), + Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", intl.formatWebComic), + ) + + return tags.sortIfTranslated(intl) + } + + internal fun getGenres(intl: MangaDexIntl): List { + val tags = listOf( + Tag("391b0423-d847-456f-aff0-8b0cfc03066b", intl.genreAction), + Tag("87cc87cd-a395-47af-b27a-93258283bbc6", intl.genreAdventure), + Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", intl.genreBoysLove), + Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", intl.genreComedy), + Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", intl.genreCrime), + Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", intl.genreDrama), + Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", intl.genreFantasy), + Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", intl.genreGirlsLove), + Tag("33771934-028e-4cb3-8744-691e866a923e", intl.genreHistorical), + Tag("cdad7e68-1419-41dd-bdce-27753074a640", intl.genreHorror), + Tag("ace04997-f6bd-436e-b261-779182193d3d", intl.genreIsekai), + Tag("81c836c9-914a-4eca-981a-560dad663e73", intl.genreMagicalGirls), + Tag("50880a9d-5440-4732-9afb-8f457127e836", intl.genreMecha), + Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", intl.genreMedical), + Tag("ee968100-4191-4968-93d3-f82d72be7e46", intl.genreMystery), + Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", intl.genrePhilosophical), + Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", intl.genreRomance), + Tag("256c8bd9-4904-4360-bf4f-508a76d67183", intl.genreSciFi), + Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", intl.genreSliceOfLife), + Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", intl.genreSports), + Tag("7064a261-a137-4d3a-8848-2d385de3a99c", intl.genreSuperhero), + Tag("07251805-a27e-4d59-b488-f0bfbec15168", intl.genreThriller), + Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", intl.genreTragedy), + Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", intl.genreWuxia), + ) + + return tags.sortIfTranslated(intl) + } + + // to get all tags from dex https://api.mangadex.org/manga/tag + internal fun getThemes(intl: MangaDexIntl): List { + val tags = listOf( + Tag("e64f6742-c834-471d-8d72-dd51fc02b835", intl.themeAliens), + Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", intl.themeAnimals), + Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", intl.themeCooking), + Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", intl.themeCrossdressing), + Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", intl.themeDelinquents), + Tag("39730448-9a5f-48a2-85b0-a70db87b1233", intl.themeDemons), + Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", intl.themeGenderSwap), + Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", intl.themeGhosts), + Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", intl.themeGyaru), + Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", intl.themeHarem), + Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", intl.themeIncest), + Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", intl.themeLoli), + Tag("85daba54-a71c-4554-8a28-9901a8b0afad", intl.themeMafia), + Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", intl.themeMagic), + Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", intl.themeMartialArts), + Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", intl.themeMilitary), + Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", intl.themeMonsterGirls), + Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", intl.themeMonsters), + Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", intl.themeMusic), + Tag("489dd859-9b61-4c37-af75-5b18e88daafc", intl.themeNinja), + Tag("92d6d951-ca5e-429c-ac78-451071cbf064", intl.themeOfficeWorkers), + Tag("df33b754-73a3-4c54-80e6-1a74a8058539", intl.themePolice), + Tag("9467335a-1b83-4497-9231-765337a00b96", intl.themePostApocalyptic), + Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", intl.themePsychological), + Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", intl.themeReincarnation), + Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", intl.themeReverseHarem), + Tag("81183756-1453-4c81-aa9e-f6e1b63be016", intl.themeSamurai), + Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", intl.themeSchoolLife), + Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", intl.themeShota), + Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", intl.themeSupernatural), + Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", intl.themeSurvival), + Tag("292e862b-2d17-4062-90a2-0356caa4ae27", intl.themeTimeTravel), + Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", intl.themeTraditionalGames), + Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", intl.themeVampires), + Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", intl.themeVideoGames), + Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", intl.themeVillainess), + Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", intl.themeVirtualReality), + Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", intl.themeZombies) + ) + + return tags.sortIfTranslated(intl) + } + + internal fun getTags(intl: MangaDexIntl): List { + return getContents(intl) + getFormats(intl) + getGenres(intl) + getThemes(intl) + } + + private data class TagMode(val title: String, val value: String) { + override fun toString(): String = title + } + + private fun getTagModes(intl: MangaDexIntl) = arrayOf( + TagMode(intl.modeAnd, "AND"), + TagMode(intl.modeOr, "OR") + ) + + private class TagInclusionMode(intl: MangaDexIntl, modes: Array) : + Filter.Select(intl.includedTagsMode, modes, 0), + UrlQueryFilter { + + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + url.addQueryParameter("includedTagsMode", values[state].value) + } + } + + private class TagExclusionMode(intl: MangaDexIntl, modes: Array) : + Filter.Select(intl.excludedTagsMode, modes, 1), + UrlQueryFilter { + + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + url.addQueryParameter("excludedTagsMode", values[state].value) + } + } + + data class Sortable(val title: String, val value: String) { + override fun toString(): String = title + } + + private fun getSortables(intl: MangaDexIntl) = arrayOf( + Sortable(intl.sortAlphabetic, "title"), + Sortable(intl.sortChapterUploadedAt, "latestUploadedChapter"), + Sortable(intl.sortNumberOfFollows, "followedCount"), + Sortable(intl.sortContentCreatedAt, "createdAt"), + Sortable(intl.sortContentInfoUpdatedAt, "updatedAt"), + Sortable(intl.sortRelevance, "relevance"), + Sortable(intl.sortYear, "year") + ) + + class SortFilter(intl: MangaDexIntl, private val sortables: Array) : + Filter.Sort( + intl.sort, + sortables.map(Sortable::title).toTypedArray(), + Selection(2, false) + ), + UrlQueryFilter { + + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + if (state != null) { + val query = sortables[state!!.index].value + val value = when (state!!.ascending) { + true -> "asc" + false -> "desc" + } + + url.addQueryParameter("order[$query]", value) + } + } + } + + private class HasAvailableChaptersFilter(intl: MangaDexIntl) : + Filter.CheckBox(intl.hasAvailableChapters), + UrlQueryFilter { + + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + if (state) { + url.addQueryParameter("hasAvailableChapters", "true") + url.addQueryParameter("availableTranslatedLanguage[]", dexLang) + } + } + } + + private class TagsFilter(intl: MangaDexIntl, innerFilters: FilterList) : + Filter.Group>(intl.tags, innerFilters), + UrlQueryFilter { + + override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { + state.filterIsInstance() + .forEach { filter -> filter.addQueryParameter(url, dexLang) } + } + } + + private fun getTagFilters(intl: MangaDexIntl): FilterList = FilterList( + TagInclusionMode(intl, getTagModes(intl)), + TagExclusionMode(intl, getTagModes(intl)), + ) + + internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String { + filters.filterIsInstance() + .forEach { filter -> filter.addQueryParameter(url, dexLang) } + return url.toString() } + + private fun List.sortIfTranslated(intl: MangaDexIntl): List = apply { + if (intl.availableLang == MangaDexIntl.ENGLISH) { + return this + } + + return sortedWith(compareBy(intl.collator, Tag::name)) + } } 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 585d6d044..cd9cd1e70 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 @@ -15,14 +15,16 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import okhttp3.CacheControl import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import org.jsoup.parser.Parser +import uy.kohesive.injekt.api.get import java.util.Date import java.util.Locale import java.util.concurrent.TimeUnit -class MangaDexHelper() { +class MangaDexHelper(private val lang: String) { val mdFilters = MangaDexFilters() @@ -34,16 +36,26 @@ class MangaDexHelper() { prettyPrint = true } + val intl = MangaDexIntl(lang) + /** * Gets the UUID from the url */ fun getUUIDFromUrl(url: String) = url.substringAfterLast("/") /** - * get chapters for manga (aka manga/$id/feed endpoint) + * Get chapters for manga (aka manga/$id/feed endpoint) */ fun getChapterEndpoint(mangaId: String, offset: Int, langCode: String) = - "${MDConstants.apiMangaUrl}/$mangaId/feed?includes[]=${MDConstants.scanlator}&includes[]=${MDConstants.uploader}&limit=500&offset=$offset&translatedLanguage[]=$langCode&order[volume]=desc&order[chapter]=desc" + "${MDConstants.apiMangaUrl}/$mangaId/feed".toHttpUrl().newBuilder() + .addQueryParameter("includes[]", MDConstants.scanlator) + .addQueryParameter("includes[]", MDConstants.uploader) + .addQueryParameter("limit", "500") + .addQueryParameter("offset", offset.toString()) + .addQueryParameter("translatedLanguage[]", langCode) + .addQueryParameter("order[volume]", "desc") + .addQueryParameter("order[chapter]", "desc") + .toString() /** * Check if the manga url is a valid uuid @@ -61,15 +73,15 @@ class MangaDexHelper() { fun getLatestChapterOffset(page: Int): String = (MDConstants.latestChapterLimit * (page - 1)).toString() /** - * Remove markdown links as well as parse any html characters in description or - * chapter name to actual characters for example ♥ will show ♥ + * Remove any HTML characters in description or chapter name to actual + * characters. For example ♥ will show ♥ */ fun cleanString(string: String): String { - val unescapedString = Parser.unescapeEntities(string, false) - - return unescapedString + return Parser.unescapeEntities(string, false) .substringBefore("---") .replace(markdownLinksRegex, "$1") + .replace(markdownItalicBoldRegex, "$1") + .replace(markdownItalicRegex, "$1") .trim() } @@ -109,6 +121,8 @@ class MangaDexHelper() { .build() val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex() + val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex() + val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex() val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex() @@ -213,7 +227,12 @@ class MangaDexHelper() { /** * Create an SManga from json element with all details */ - fun createManga(mangaDataDto: MangaDataDto, chapters: List, lang: String, coverSuffix: String?): SManga { + fun createManga( + mangaDataDto: MangaDataDto, + chapters: List, + lang: String, + coverSuffix: String? + ): SManga { try { val attr = mangaDataDto.attributes @@ -224,7 +243,7 @@ class MangaDexHelper() { if (tempContentRating == null || tempContentRating.equals("safe", true)) { null } else { - "Content rating: " + tempContentRating.capitalize(Locale.US) + intl.contentRatingGenre(tempContentRating) } val dexLocale = Locale.forLanguageTag(lang) @@ -237,44 +256,53 @@ class MangaDexHelper() { .capitalize(dexLocale) ) - val authors = mangaDataDto.relationships.filter { relationshipDto -> - relationshipDto.type.equals(MDConstants.author, true) - }.mapNotNull { it.attributes!!.name }.distinct() + val authors = mangaDataDto.relationships + .filter { relationshipDto -> + relationshipDto.type.equals(MDConstants.author, true) + } + .mapNotNull { it.attributes!!.name }.distinct() - val artists = mangaDataDto.relationships.filter { relationshipDto -> - relationshipDto.type.equals(MDConstants.artist, true) - }.mapNotNull { it.attributes!!.name }.distinct() + val artists = mangaDataDto.relationships + .filter { relationshipDto -> + relationshipDto.type.equals(MDConstants.artist, true) + } + .mapNotNull { it.attributes!!.name }.distinct() - val coverFileName = mangaDataDto.relationships.firstOrNull { relationshipDto -> - relationshipDto.type.equals(MDConstants.coverArt, true) - }?.attributes?.fileName + val coverFileName = mangaDataDto.relationships + .firstOrNull { relationshipDto -> + relationshipDto.type.equals(MDConstants.coverArt, true) + } + ?.attributes + ?.fileName - // get tag list - val tags = mdFilters.getTags() + val tags = mdFilters.getTags(intl) - // map ids to tag names - val genreList = ( - attr.tags - .map { it.id } - .map { dexId -> - tags.firstOrNull { it.id == dexId } - } - .map { it?.name } + - nonGenres - ) - .filter { it.isNullOrBlank().not() } + 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() + + val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + + nonGenres.filterNotNull() val desc = attr.description.asMdMap() + return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply { description = cleanString(desc[lang] ?: desc["en"] ?: "") author = authors.joinToString(", ") artist = artists.joinToString(", ") status = getPublicationStatus(attr, chapters) - genre = genreList.joinToString(", ") + genre = genreList + .filter(String::isNotEmpty) + .joinToString(", ") } } catch (e: Exception) { Log.e("MangaDex", "error parsing manga", e) - throw(e) + throw e } } @@ -295,14 +323,21 @@ class MangaDexHelper() { .joinToString(" & ") .ifEmpty { // fall back to uploader name if no group - val users = chapterDataDto.relationships.filter { relationshipDto -> - relationshipDto.type.equals( - MDConstants.uploader, - true - ) - }.mapNotNull { it.attributes!!.username } - users.joinToString(" & ", if (users.isNotEmpty()) "Uploaded by " else "") - }.ifEmpty { "No Group" } // "No Group" as final resort + val users = chapterDataDto.relationships + .filter { relationshipDto -> + relationshipDto.type.equals( + MDConstants.uploader, + true + ) + } + .mapNotNull { it.attributes!!.username } + + users.joinToString( + " & ", + if (users.isNotEmpty()) intl.uploadedBy(users.toString()) else "" + ) + } + .ifEmpty { intl.noGroup } // "No Group" as final resort val chapterName = mutableListOf() // Build chapter name @@ -347,7 +382,7 @@ class MangaDexHelper() { } } catch (e: Exception) { Log.e("MangaDex", "error parsing chapter", e) - throw(e) + throw e } } 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 new file mode 100644 index 000000000..b1c5e6fa0 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexIntl.kt @@ -0,0 +1,661 @@ +package eu.kanade.tachiyomi.extension.all.mangadex + +import java.text.Collator +import java.util.Locale + +class MangaDexIntl(val lang: String) { + + val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH + + val locale: Locale = Locale.forLanguageTag(availableLang) + + val collator: Collator = Collator.getInstance(locale) + + val invalidGroupId: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "ID do grupo inválido" + else -> "Not a valid group ID" + } + + val invalidAuthorId: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "ID do autor inválido" + else -> "Not a valid author ID" + } + + val noSeriesInList: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sem séries na lista" + else -> "No series in the list" + } + + val migrateWarning: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> + "Migre esta entrada do $MANGADEX_NAME para o $MANGADEX_NAME para atualizar" + else -> "Migrate this entry from $MANGADEX_NAME to $MANGADEX_NAME to update it" + } + + val coverQuality: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Qualidade da capa" + else -> "Cover quality" + } + + val coverQualityOriginal: String = "Original" + + val coverQualityMedium: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Média" + else -> "Medium" + } + + val coverQualityLow: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Baixa" + else -> "Low" + } + + val dataSaver: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Economia de dados" + else -> "Data saver" + } + + val dataSaverSummary: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Utiliza imagens menores e mais compactadas" + else -> "Enables smaller, more compressed images" + } + + val standardHttpsPort: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Utilizar somente a porta 443 do HTTPS" + else -> "Use HTTPS port 443 only" + } + + val standardHttpsPortSummary: String = when (availableLang) { + 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." + else -> + "Enable to only request image servers that use port 443. This allows users with " + + "stricter firewall restrictions to access MangaDex images" + } + + val contentRating: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação de conteúdo" + else -> "Content rating" + } + + val standardContentRating: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação de conteúdo padrão" + else -> "Default content rating" + } + + val standardContentRatingSummary: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> + "Mostra os conteúdos com as classificações selecionadas por padrão" + else -> "Show content with the selected ratings by default" + } + + val contentRatingSafe: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Seguro" + else -> "Safe" + } + + val contentRatingSuggestive: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sugestivo" + else -> "Suggestive" + } + + val contentRatingErotica: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Erótico" + else -> "Erotica" + } + + val contentRatingPornographic: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Pornográfico" + else -> "Pornographic" + } + + private val contentRatingMap: Map = mapOf( + "safe" to contentRatingSafe, + "suggestive" to contentRatingSuggestive, + "erotica" to contentRatingErotica, + "pornographic" to contentRatingPornographic + ) + + fun contentRatingGenre(contentRatingKey: String): String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação: ${contentRatingMap[contentRatingKey]}" + else -> "$contentRating: ${contentRatingMap[contentRatingKey]}" + } + + val originalLanguage: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Idioma original" + else -> "Original language" + } + + val originalLanguageFilterJapanese: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "${languageDisplayName(JAPANESE)} (Mangá)" + else -> "${languageDisplayName(JAPANESE)} (Manga)" + } + + val originalLanguageFilterChinese: String = "${languageDisplayName(CHINESE)} (Manhua)" + + val originalLanguageFilterKorean: String = "${languageDisplayName(KOREAN)} (Manhwa)" + + val filterOriginalLanguages: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Filtrar os idiomas originais" + else -> "Filter original languages" + } + + val filterOriginalLanguagesSummary: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> + "Mostra somente conteúdos que foram publicados originalmente nos idiomas " + + "selecionados nas seções de recentes e navegar" + else -> + "Only show content that was originally published in the selected languages in " + + "both latest and browse" + } + + val blockGroupByUuid: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Bloquear grupos por UUID" + else -> "Block groups by UUID" + } + + val blockGroupByUuidSummary: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> + "Capítulos de grupos bloqueados não irão aparecer no feed de Recentes ou Mangás. " + + "Digite uma lista de UUIDs dos grupos separados por vírgulas" + else -> + "Chapters from blocked groups will not show up in Latest or Manga feed. " + + "Enter as a Comma-separated list of group UUIDs" + } + + val blockUploaderByUuid: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Bloquear uploaders por UUID" + else -> "Block uploader by UUID" + } + + val blockUploaderByUuidSummary: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> + "Capítulos de usuários bloqueados não irão aparecer no feed de Recentes ou Mangás. " + + "Digite uma lista de UUIDs dos usuários separados por vírgulas" + else -> + "Chapters from blocked uploaders will not show up in Latest or Manga feed. " + + "Enter as a Comma-separated list of uploader UUIDs" + } + + val publicationDemographic: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Demografia da publicação" + else -> "Publication demographic" + } + + val publicationDemographicNone: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Nenhuma" + else -> "None" + } + + val publicationDemographicShounen: String = "Shounen" + + val publicationDemographicShoujo: String = "Shoujo" + + val publicationDemographicSeinen: String = "Seinen" + + val publicationDemographicJosei: String = "Josei" + + val status: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Estado" + else -> "Status" + } + + val statusOngoing: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Em andamento" + else -> "Ongoing" + } + + val statusCompleted: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Completo" + else -> "Completed" + } + + val statusHiatus: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Hiato" + else -> "Hiatus" + } + + val statusCancelled: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Cancelado" + else -> "Cancelled" + } + + val content: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Conteúdo" + else -> "Content" + } + + val contentGore: String = "Gore" + + val contentSexualViolence: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Violência sexual" + else -> "Sexual Violence" + } + + val format: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Formato" + else -> "Format" + } + + val formatAdaptation: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Adaptação" + else -> "Adaptation" + } + + val formatAnthology: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Antologia" + else -> "Anthology" + } + + val formatAwardWinning: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Premiado" + else -> "Award Winning" + } + + val formatDoujinshi: String = "Doujinshi" + + val formatFanColored: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Colorizado por fãs" + else -> "Fan Colored" + } + + val formatFourKoma: String = "4-Koma" + + val formatFullColor: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Colorido" + else -> "Full Color" + } + + val formatLongStrip: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vertical" + else -> "Long Strip" + } + + val formatOfficialColored: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Colorizado oficialmente" + else -> "Official Colored" + } + + val formatOneshot: String = "Oneshot" + + val formatUserCreated: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Criado por usuários" + else -> "User Created" + } + + val formatWebComic: String = "Web Comic" + + val genre: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Gênero" + else -> "Genre" + } + + val genreAction: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ação" + else -> "Action" + } + + val genreAdventure: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Aventura" + else -> "Adventure" + } + + val genreBoysLove: String = "Boy's Love" + + val genreComedy: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Comédia" + else -> "Comedy" + } + + val genreCrime: String = "Crime" + + val genreDrama: String = "Drama" + + val genreFantasy: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Fantasia" + else -> "Fantasy" + } + + val genreGirlsLove: String = "Girl's Love" + + val genreHistorical: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Histórico" + else -> "Historical" + } + + val genreHorror: String = "Horror" + + val genreIsekai: String = "Isekai" + + val genreMagicalGirls: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Garotas mágicas" + else -> "Magical Girls" + } + + val genreMecha: String = "Mecha" + + val genreMedical: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Médico" + else -> "Medical" + } + + val genreMystery: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Mistério" + else -> "Mystery" + } + + val genrePhilosophical: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Filosófico" + else -> "Philosophical" + } + + val genreRomance: String = "Romance" + + val genreSciFi: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ficção científica" + else -> "Sci-Fi" + } + + val genreSliceOfLife: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Cotidiano" + else -> "Slice of Life" + } + + val genreSports: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Esportes" + else -> "Sports" + } + + val genreSuperhero: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Super-heroi" + else -> "Superhero" + } + + val genreThriller: String = "Thriller" + + val genreTragedy: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Tragédia" + else -> "Tragedy" + } + + val genreWuxia: String = "Wuxia" + + val theme: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Tema" + else -> "Theme" + } + + val themeAliens: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Alienígenas" + else -> "Aliens" + } + + val themeAnimals: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Animais" + else -> "Animals" + } + + val themeCooking: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Culinária" + else -> "Cooking" + } + + val themeCrossdressing: String = "Crossdressing" + + val themeDelinquents: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Delinquentes" + else -> "Delinquents" + } + + val themeDemons: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Demônios" + else -> "Demons" + } + + val themeGenderSwap: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Troca de gêneros" + else -> "Genderswap" + } + + val themeGhosts: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Fantasmas" + else -> "Ghosts" + } + + val themeGyaru: String = "Gyaru" + + val themeHarem: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Harém" + else -> "Harem" + } + + val themeIncest: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Incesto" + else -> "Incest" + } + + val themeLoli: String = "Loli" + + val themeMafia: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Máfia" + else -> "Mafia" + } + + val themeMagic: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Magia" + else -> "Magic" + } + + val themeMartialArts: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Artes marciais" + else -> "Martial Arts" + } + + val themeMilitary: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Militar" + else -> "Military" + } + + val themeMonsterGirls: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Garotas monstro" + else -> "Monster Girls" + } + + val themeMonsters: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Monstros" + else -> "Monsters" + } + + val themeMusic: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Musical" + else -> "Music" + } + + val themeNinja: String = "Ninja" + + val themeOfficeWorkers: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Funcionários de escritório" + else -> "Office Workers" + } + + val themePolice: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Policial" + else -> "Police" + } + + val themePostApocalyptic: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Pós-apocalíptico" + else -> "Post-Apocalypytic" + } + + val themePsychological: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Psicológico" + else -> "Psychological" + } + + val themeReincarnation: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Reencarnação" + else -> "Reincarnation" + } + + val themeReverseHarem: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Harém reverso" + else -> "Reverse Harem" + } + + val themeSamurai: String = "Samurai" + + val themeSchoolLife: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vida escolar" + else -> "School Life" + } + + val themeShota: String = "Shota" + + val themeSupernatural: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sobrenatural" + else -> "Supernatural" + } + + val themeSurvival: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sobrevivência" + else -> "Survival" + } + + val themeTimeTravel: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Viagem no tempo" + else -> "Time Travel" + } + + val themeTraditionalGames: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Jogos tradicionais" + else -> "Traditional Games" + } + + val themeVampires: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vampiros" + else -> "Vampires" + } + + val themeVideoGames: String = "Video Games" + + val themeVillainess: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vilania" + else -> "Villainess" + } + + val themeVirtualReality: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Realidade virtual" + else -> "Virtual Reality" + } + + val themeZombies: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Zumbis" + else -> "Zombies" + } + + val tags: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Modo das tags" + else -> "Tags mode" + } + + val includedTagsMode: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Modo de inclusão de tags" + else -> "Included tags mode" + } + + val excludedTagsMode: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Modo de exclusão de tags" + else -> "Excluded tags mode" + } + + val modeAnd: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "E" + else -> "And" + } + + val modeOr: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ou" + else -> "Or" + } + + val sort: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ordenar" + else -> "Sort" + } + + val sortAlphabetic: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Alfabeticamente" + else -> "Alphabetic" + } + + val sortChapterUploadedAt: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Upload do capítulo" + else -> "Chapter uploaded at" + } + + val sortNumberOfFollows: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Número de seguidores" + else -> "Number of follows" + } + + val sortContentCreatedAt: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Criação do conteúdo" + else -> "Content created at" + } + + val sortContentInfoUpdatedAt: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Atualização das informações" + else -> "Content info updated at" + } + + val sortRelevance: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Relevância" + else -> "Relevance" + } + + val sortYear: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ano de lançamento" + else -> "Year" + } + + val hasAvailableChapters: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Há capítulos disponíveis" + else -> "Has available chapters" + } + + fun languageDisplayName(localeCode: String): String = + Locale.forLanguageTag(localeCode) + .getDisplayName(locale) + .capitalize(locale) + + fun unableToProcessChapterRequest(code: Int): String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> + "Não foi possível processar a requisição do capítulo. Código HTTP: $code" + else -> "Unable to process Chapter request. HTTP code: $code" + } + + fun uploadedBy(user: String): String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Enviado por $user" + else -> "Uploaded by $user" + } + + val noGroup: String = when (availableLang) { + BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sem grupo" + else -> "No Group" + } + + companion object { + const val BRAZILIAN_PORTUGUESE = "pt-BR" + const val CHINESE = "zh" + const val ENGLISH = "en" + const val JAPANESE = "ja" + const val KOREAN = "ko" + const val PORTUGUESE = "pt" + + val AVAILABLE_LANGS = arrayOf(BRAZILIAN_PORTUGUESE, ENGLISH, PORTUGUESE) + + const val MANGADEX_NAME = "MangaDex" + } +} 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 ebf2aa4ae..25f8938e7 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 @@ -59,6 +59,12 @@ data class MangaAttributesDto( @Serializable data class TagDto( val id: String, + val attributes: TagAttributesDto +) + +@Serializable +data class TagAttributesDto( + val group: String ) fun JsonElement.asMdMap(): Map {