diff --git a/src/all/mangadex/build.gradle b/src/all/mangadex/build.gradle index 0e54b398b..0a924f5ff 100644 --- a/src/all/mangadex/build.gradle +++ b/src/all/mangadex/build.gradle @@ -5,7 +5,7 @@ ext { extName = 'MangaDex' pkgNameSuffix = 'all.mangadex' extClass = '.MangaDexFactory' - extVersionCode = 105 + extVersionCode = 106 libVersion = '1.2' containsNsfw = 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 new file mode 100644 index 000000000..2ce2b8ee5 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.extension.all.mangadex + +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +class MDConstants { + + companion object { + val uuidRegex = + Regex("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}") + + val mangaLimit = 25 + val apiUrl = "https://api.mangadex.org" + val apiMangaUrl = "$apiUrl/manga" + val atHomePostUrl = "https://api.mangadex.network/report" + val whitespaceRegex = "\\s".toRegex() + + val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US) + .apply { timeZone = TimeZone.getTimeZone("UTC") } + + const val prefixIdSearch = "id:" + + const val dataSaverPrefTitle = "Data saver" + const val dataSaverPref = "dataSaver" + + const val mdAtHomeTokenLifespan = 10 * 60 * 1000 + } +} 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 23cd5c2ad..75bf9fd47 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 @@ -4,1067 +4,260 @@ import android.app.Application import android.content.SharedPreferences import android.util.Log import com.github.salomonbrys.kotson.array -import com.github.salomonbrys.kotson.bool import com.github.salomonbrys.kotson.get import com.github.salomonbrys.kotson.int -import com.github.salomonbrys.kotson.long -import com.github.salomonbrys.kotson.nullString +import com.github.salomonbrys.kotson.obj import com.github.salomonbrys.kotson.string -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonElement -import com.google.gson.JsonObject import com.google.gson.JsonParser -import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.ParsedHttpSource -import eu.kanade.tachiyomi.util.asJsoup +import eu.kanade.tachiyomi.source.online.HttpSource import okhttp3.CacheControl import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.Interceptor -import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.RequestBody import okhttp3.Response -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import org.jsoup.parser.Parser import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.net.URLEncoder import java.util.Date -import java.util.concurrent.TimeUnit -import kotlin.collections.set - -abstract class MangaDex( - override val lang: String, - private val internalLang: String -) : ConfigurableSource, ParsedHttpSource() { +abstract class MangaDex(override val lang: String) : ConfigurableSource, HttpSource() { override val name = "MangaDex" - override val baseUrl = "https://www.mangadex.org" - private val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org" + // after mvp comes out make current popular becomes latest (mvp doesnt have a browse page) + override val supportsLatest = false - override val supportsLatest = true + private val helper = MangaDexHelper() private val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } - private val mangadexDescription: MangadexDescription by lazy { - MangadexDescription(internalLang) - } - - private val rateLimitInterceptor = MdRateLimitInterceptor() - - override val client: OkHttpClient = network.client.newBuilder() - .addNetworkInterceptor(rateLimitInterceptor) - .addInterceptor(CoverInterceptor()) - .addInterceptor(MdAtHomeReportInterceptor(network.client, headersBuilder().build())) - .build() - - private fun clientBuilder(): OkHttpClient = clientBuilder(getShowR18()) - - private fun clientBuilder(r18Toggle: Int): OkHttpClient = network.client.newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .addNetworkInterceptor(rateLimitInterceptor) - .addNetworkInterceptor { chain -> - val originalCookies = chain.request().header("Cookie") ?: "" - val newReq = chain - .request() - .newBuilder() - .header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}") - .build() - chain.proceed(newReq) - }.build()!! - override fun headersBuilder() = Headers.Builder().apply { add("User-Agent", "Tachiyomi " + System.getProperty("http.agent")) } - private fun cookiesHeader(r18Toggle: Int): String { - val cookies = mutableMapOf() - cookies["mangadex_h_toggle"] = r18Toggle.toString() - return buildCookies(cookies) - } + override val client = + network.client.newBuilder().addNetworkInterceptor(mdRateLimitInterceptor).addInterceptor( + coverInterceptor + ).addInterceptor(MdAtHomeReportInterceptor(network.client, headersBuilder().build())) + .build() - private fun buildCookies(cookies: Map) = - cookies.entries.joinToString(separator = "; ", postfix = ";") { - "${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}" - } + // POPULAR Manga Section - override fun popularMangaSelector() = "div.manga-entry" - - override fun latestUpdatesSelector() = "tr a.manga_title" - - // url matches default SortFilter selection (Rating Descending) override fun popularMangaRequest(page: Int): Request { - return GET("$baseUrl/titles/7/$page/", headersBuilder().build(), CacheControl.FORCE_NETWORK) + return GET( + url = "${MDConstants.apiMangaUrl}?order[updatedAt]=desc&limit=${MDConstants.mangaLimit}&offset=${ + helper.getMangaListOffset( + page + ) + }", + headers = headers, + cache = CacheControl.FORCE_NETWORK + ) } - override fun latestUpdatesRequest(page: Int): Request { - return GET("$baseUrl/updates/$page", headersBuilder().build(), CacheControl.FORCE_NETWORK) - } - - override fun popularMangaFromElement(element: Element): SManga { - val manga = SManga.create() - element.select("a.manga_title").first().let { - val url = modifyMangaUrl(it.attr("href")) - manga.setUrlWithoutDomain(url) - manga.title = it.text().trim() - } - manga.thumbnail_url = formThumbUrl(manga.url) - return manga - } - - private fun modifyMangaUrl(url: String): String = - url.replace("/title/", "/manga/").substringBeforeLast("/") + "/" - - private fun formThumbUrl(mangaUrl: String): String { - var ext = ".jpg" - - if (getShowThumbnail() == LOW_QUALITY) { - ext = ".thumb$ext" + override fun popularMangaParse(response: Response): MangasPage { + if (response.isSuccessful.not()) { + throw Exception("Error getting popular manga http code: ${response.code}") } - return cdnUrl + "/images/manga/" + getMangaId(mangaUrl) + ext + val mangaListResponse = JsonParser.parseString(response.body!!.string()).obj + val hasMoreResults = + (mangaListResponse["limit"].int + mangaListResponse["offset"].int) < mangaListResponse["total"].int + + val mangaList = mangaListResponse["results"].array.map { helper.createManga(it) } + return MangasPage(mangaList, hasMoreResults) } - override fun latestUpdatesFromElement(element: Element): SManga { - val manga = SManga.create() - element.let { - manga.setUrlWithoutDomain(modifyMangaUrl(it.attr("href"))) - manga.title = it.text().trim() - } - manga.thumbnail_url = formThumbUrl(manga.url) + // LATEST section API can't sort by date yet so not implemented + override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used") - return manga - } + override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") - override fun popularMangaNextPageSelector() = - ".pagination li:not(.disabled) span[title*=last page]:not(disabled)" - - override fun latestUpdatesNextPageSelector() = - ".pagination li:not(.disabled) span[title*=last page]:not(disabled)" - - override fun searchMangaNextPageSelector() = - ".pagination li:not(.disabled) span[title*=last page]:not(disabled)" - - override fun fetchPopularManga(page: Int): Observable { - return clientBuilder().newCall(popularMangaRequest(page)) - .asObservableSuccess() - .map { response -> - popularMangaParse(response) - } - } - - override fun fetchLatestUpdates(page: Int): Observable { - return clientBuilder().newCall(latestUpdatesRequest(page)) - .asObservableSuccess() - .map { response -> - latestUpdatesParse(response) - } - } - - override fun fetchSearchManga( - page: Int, - query: String, - filters: FilterList - ): Observable { - return if (query.startsWith(PREFIX_ID_SEARCH)) { - val realQuery = query.removePrefix(PREFIX_ID_SEARCH) - client.newCall(searchMangaByIdRequest(realQuery)) - .asObservableSuccess() - .map { response -> - val details = mangaDetailsParse(response) - details.url = "/manga/$realQuery/" - MangasPage(listOf(details), false) - } - } else { - getSearchClient(filters).newCall(searchMangaRequest(page, query, filters)) - .asObservableSuccess() - .map { response -> - searchMangaParse(response) - } - } - } - - private fun getSearchClient(filters: FilterList): OkHttpClient { - filters.forEach { filter -> - when (filter) { - is R18 -> { - return when (filter.state) { - 1 -> clientBuilder(ALL) - 2 -> clientBuilder(ONLY_R18) - 3 -> clientBuilder(NO_R18) - else -> clientBuilder() - } - } - } - } - return clientBuilder() - } - - private var groupSearch: String? = null + // SEARCH section override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - if (page == 1) groupSearch = null - val genresToInclude = mutableListOf() - val genresToExclude = mutableListOf() + if (query.startsWith(MDConstants.prefixIdSearch)) { + val url = MDConstants.apiMangaUrl.toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("ids[]", query.removePrefix(MDConstants.prefixIdSearch)) + return GET(url.toString(), headers, CacheControl.FORCE_NETWORK) + } - // Do traditional search - val url = "$baseUrl/?page=search".toHttpUrlOrNull()!!.newBuilder() - .addQueryParameter("p", page.toString()) - .addQueryParameter("title", query.replace(WHITESPACE_REGEX, " ")) + val tempUrl = MDConstants.apiMangaUrl.toHttpUrlOrNull()!!.newBuilder() - filters.forEach { filter -> - when (filter) { - is TextField -> url.addQueryParameter(filter.key, filter.state) - is Demographic -> { - val demographicToInclude = mutableListOf() - filter.state.forEach { content -> - if (content.isIncluded()) { - demographicToInclude.add(content.id) - } - } - if (demographicToInclude.isNotEmpty()) { - url.addQueryParameter("demos", demographicToInclude.joinToString(",")) - } - } - is PublicationStatus -> { - val publicationToInclude = mutableListOf() - filter.state.forEach { content -> - if (content.isIncluded()) { - publicationToInclude.add(content.id) - } - } - if (publicationToInclude.isNotEmpty()) { - url.addQueryParameter("statuses", publicationToInclude.joinToString(",")) - } - } - is OriginalLanguage -> { - if (filter.state != 0) { - val number: String = - SOURCE_LANG_LIST.first { it.first == filter.values[filter.state] }.second - url.addQueryParameter("lang_id", number) - } - } - is TagInclusionMode -> { - url.addQueryParameter("tag_mode_inc", arrayOf("all", "any")[filter.state]) - } - is TagExclusionMode -> { - url.addQueryParameter("tag_mode_exc", arrayOf("all", "any")[filter.state]) - } - is ContentList -> { - filter.state.forEach { content -> - if (content.isExcluded()) { - genresToExclude.add(content.id) - } else if (content.isIncluded()) { - genresToInclude.add(content.id) - } - } - } - is FormatList -> { - filter.state.forEach { format -> - if (format.isExcluded()) { - genresToExclude.add(format.id) - } else if (format.isIncluded()) { - genresToInclude.add(format.id) - } - } - } - is GenreList -> { - filter.state.forEach { genre -> - if (genre.isExcluded()) { - genresToExclude.add(genre.id) - } else if (genre.isIncluded()) { - genresToInclude.add(genre.id) - } - } - } - is ThemeList -> { - filter.state.forEach { theme -> - if (theme.isExcluded()) { - genresToExclude.add(theme.id) - } else if (theme.isIncluded()) { - genresToInclude.add(theme.id) - } - } - } - is SortFilter -> { - if (filter.state != null) { - if (filter.state!!.ascending) { - url.addQueryParameter( - "s", - sortables[filter.state!!.index].second.toString() - ) - } else { - url.addQueryParameter( - "s", - sortables[filter.state!!.index].third.toString() - ) - } - } - } - is ScanGroup -> { - groupSearch = when { - filter.state.isNotEmpty() && page == 1 -> "$baseUrl/groups/0/1/${filter.state}" - filter.state.isNotEmpty() && page > 1 -> "${groupSearch!!}/$page" - else -> null - } - } + tempUrl.apply { + addQueryParameter("limit", MDConstants.mangaLimit.toString()) + addQueryParameter("offset", (helper.getMangaListOffset(page))) + val actualQuery = query.replace(MDConstants.whitespaceRegex, " ") + if (actualQuery.isNotBlank()) { + addQueryParameter("title", actualQuery) } } - // Manually append genres list to avoid commas being encoded - var urlToUse = url.toString() - if (genresToInclude.isNotEmpty()) { - urlToUse += "&tags_inc=" + genresToInclude.joinToString(",") - } - if (genresToExclude.isNotEmpty()) { - urlToUse += "&tags_exc=" + genresToExclude.joinToString(",") - } + val finalUrl = helper.mdFilters.addFiltersToUrl(tempUrl, filters) - return GET(groupSearch ?: urlToUse, headersBuilder().build(), CacheControl.FORCE_NETWORK) + return GET(finalUrl, headers, CacheControl.FORCE_NETWORK) } - override fun searchMangaParse(response: Response): MangasPage { - return if (response.request.url.toString().contains("/groups/")) { - response.asJsoup() - .select(".table > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(2) > a") - .firstOrNull()?.attr("abs:href") - ?.let { - groupSearch = "$it/manga/0" - super.searchMangaParse( - client.newCall( - GET( - groupSearch!!, - headersBuilder().build() - ) - ).execute() - ) - } - ?: MangasPage(emptyList(), false) - } else { - val document = response.asJsoup() - if (document.select("#login_button") - .isNotEmpty() - ) throw Exception("Log in via WebView to enable search") + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) - val mangas = document.select(searchMangaSelector()).map { element -> - searchMangaFromElement(element) - } - - val hasNextPage = searchMangaNextPageSelector().let { selector -> - document.select(selector).first() - } != null - - MangasPage(mangas, hasNextPage) - } - } - - override fun searchMangaSelector() = "div.manga-entry" - - override fun searchMangaFromElement(element: Element): SManga { - val manga = SManga.create() - - element.select("a.manga_title").first().let { - val url = modifyMangaUrl(it.attr("href")) - manga.setUrlWithoutDomain(url) - manga.title = it.text().trim() - } - - manga.thumbnail_url = formThumbUrl(manga.url) - - return manga - } + // Manga Details section + // Shenanigans to allow "open in webview" to show a webpage instead of JSON override fun fetchMangaDetails(manga: SManga): Observable { - return clientBuilder().newCall(apiRequest(manga)) + return client.newCall(apiMangaDetailsRequest(manga)) .asObservableSuccess() .map { response -> mangaDetailsParse(response).apply { initialized = true } } } - private fun apiRequest(manga: SManga): Request { - return GET( - API_URL + API_MANGA + getMangaId(manga.url) + API_MANGA_INCLUDE_CHAPTERS, - headers, - CacheControl.FORCE_NETWORK - ) + override fun mangaDetailsRequest(manga: SManga): Request { + return GET("${baseUrl}${manga.url}", headers) } - - private fun searchMangaByIdRequest(id: String): Request { - return GET(API_URL + API_MANGA + id + API_MANGA_INCLUDE_CHAPTERS, headers, CacheControl.FORCE_NETWORK) - } - - private fun getMangaId(url: String): String { - val lastSection = url.trimEnd('/').substringAfterLast("/") - return if (lastSection.toIntOrNull() != null) { - lastSection - } else { - // this occurs if person has manga from before that had the id/name/ - url.trimEnd('/').substringBeforeLast("/").substringAfterLast("/") + /** + * get manga details url throws exception if the url is the old format so people migrate + */ + fun apiMangaDetailsRequest(manga: SManga): Request { + if (!helper.containsUuid(manga.url.trim())) { + throw Exception("Migrate this manga from MangaDex to MangaDex to update it") } + return GET("${MDConstants.apiUrl}${manga.url}", headers, CacheControl.FORCE_NETWORK) } override fun mangaDetailsParse(response: Response): SManga { - val manga = SManga.create() - val jsonData = response.body!!.string() - val json = JsonParser().parse(jsonData).asJsonObject["data"] - val mangaJson = json["manga"].asJsonObject - val chapterJson = json["chapters"].asJsonArray - manga.title = cleanString(mangaJson["title"].string) - manga.thumbnail_url = mangaJson["mainCover"].string - manga.description = - cleanString(mangadexDescription.clean(mangaJson["description"].string)) - manga.author = cleanString(mangaJson["author"].array.map { it.string }.joinToString()) - manga.artist = cleanString(mangaJson["artist"].array.map { it.string }.joinToString()) - val status = mangaJson["publication"]["status"].int - val finalChapterNumber = getFinalChapter(mangaJson) - if ((status == 2 || status == 3) && chapterJson != null && isMangaCompleted( - chapterJson, - finalChapterNumber - ) - ) { - manga.status = SManga.COMPLETED - } else if (status == 2 && chapterJson != null && isOneshot( - chapterJson, - finalChapterNumber - ) - ) { - manga.status = SManga.COMPLETED - } else { - manga.status = parseStatus(status) + val manga = JsonParser.parseString(response.body!!.string()).obj + return helper.createManga(manga, client) + } + + // Chapter list section + /** + * get chapter list if manga url is old format throws exception + */ + override fun chapterListRequest(manga: SManga): Request { + if (!helper.containsUuid(manga.url)) { + throw Exception("Migrate this manga from MangaDex to MangaDex to update it") } - - val genres = if (mangaJson["isHentai"].bool) { - listOf("Hentai") - } else { - listOf() - } + - mangaJson["tags"].array.mapNotNull { GENRES[it.string] } + - mangaJson["publication"]["language"].string - manga.genre = genres.joinToString(", ") - return manga - } - - // Remove bbcode tags as well as parses any html characters in description or chapter name to actual characters for example ♥ will show ♥ - private fun cleanString(string: String): String { - val bbRegex = - """\[(\w+)[^]]*](.*?)\[/\1]""".toRegex() - var intermediate = string - .replace("[list]", "") - .replace("[/list]", "") - .replace("[*]", "") - // Recursively remove nested bbcode - while (bbRegex.containsMatchIn(intermediate)) { - intermediate = intermediate.replace(bbRegex, "$2") - } - return Parser.unescapeEntities(intermediate, false) - } - - override fun mangaDetailsParse(document: Document) = throw Exception("Not Used") - - override fun chapterListSelector() = "" - - override fun fetchChapterList(manga: SManga): Observable> { - return clientBuilder().newCall(apiRequest(manga)) - .asObservableSuccess() - .map { response -> - chapterListParse(response) - } - } - - private fun getFinalChapter(jsonObj: JsonObject): String = - jsonObj.get("lastChapter").nullString?.trim() ?: "" - - private fun isOneshot(chapterJson: JsonArray, lastChapter: String): Boolean { - val chapter = - chapterJson.takeIf { it.size() > 0 }?.elementAt(0)?.asJsonObject?.get("title")?.string - return if (chapter != null) { - chapter == "Oneshot" || chapter.isEmpty() && lastChapter == "0" - } else { - false - } - } - - private fun isMangaCompleted(chapterJson: JsonArray, finalChapterNumber: String): Boolean { - val count = chapterJson - .filter { it.asJsonObject.get("language").string == internalLang } - .filter { doesFinalChapterExist(finalChapterNumber, it) }.count() - return count != 0 - } - - private fun doesFinalChapterExist(finalChapterNumber: String, chapterJson: JsonElement) = - finalChapterNumber.isNotEmpty() && finalChapterNumber == chapterJson["chapter"].string.trim() - - override fun chapterListParse(response: Response): List { - val now = Date().time - val jsonData = response.body!!.string() - val json = JsonParser().parse(jsonData).asJsonObject["data"] - val mangaJson = json["manga"].asJsonObject - - val status = mangaJson["publication"]["status"].int - - val finalChapterNumber = getFinalChapter(mangaJson) - - val chapterJson = json["chapters"].asJsonArray - val chapters = mutableListOf() - - // Skip chapters that don't match the desired language, or are future releases - val groups = json["groups"].array.map { - val group = it.asJsonObject - Pair(group["id"].int, group["name"].string) - }.toMap() - - val hasMangaPlus = groups.containsKey(9097) - - chapterJson?.forEach { jsonElement -> - val chapterElement = jsonElement.asJsonObject - if (shouldKeepChapter(chapterElement, now)) { - chapters.add(chapterFromJson(chapterElement, finalChapterNumber, status, groups)) - } - } - return chapters.also { if (it.isEmpty() && hasMangaPlus) throw Exception("This only has MangaPlus chapters, use the MangaPlus extension") } + return actualChapterListRequest(helper.getUUIDFromUrl(manga.url), 0) } /** - * Filter out the following chapters: - * language doesn't match the chosen language - * Future chapters - * Chapters from MangaPlus since they have to be read in MangaPlus extension + * Required because api is paged */ - private fun shouldKeepChapter(chapterJson: JsonObject, now: Long): Boolean { - return when { - chapterJson["language"].string != internalLang -> false - (chapterJson["timestamp"].asLong * 1000) > now -> false - chapterJson["groups"].array.map { it.string }.contains("9097") -> false - else -> true - } - } + private fun actualChapterListRequest(mangaId: String, offset: Int) = + GET( + url = helper.getChapterEndpoint(mangaId, offset, lang), + headers = headers, + cache = CacheControl.FORCE_NETWORK + ) - private fun chapterFromJson( - chapterJson: JsonObject, - finalChapterNumber: String, - status: Int, - groups: Map - ): SChapter { - val chapter = SChapter.create() - chapter.url = OLD_API_CHAPTER + chapterJson["id"].string - val chapterName = mutableListOf() - // Build chapter name - if (chapterJson["volume"].string.isNotBlank()) { - chapterName.add("Vol." + chapterJson.get("volume").string) + override fun chapterListParse(response: Response): List { + + if (response.isSuccessful.not()) { + throw Exception("Error getting chapter list http code: ${response.code}") } - if (chapterJson["chapter"].string.isNotBlank()) { - chapterName.add("Ch." + chapterJson.get("chapter").string) - } - if (chapterJson["title"].string.isNotBlank()) { - if (chapterName.isNotEmpty()) { - chapterName.add("-") + try { + val chapterListResponse = JsonParser.parseString(response.body!!.string()).obj + + val chapterListResults = chapterListResponse["results"].array.map { it.obj }.toMutableList() + + val mangaId = + response.request.url.toString().substringBefore("/feed") + .substringAfter(MDConstants.apiMangaUrl) + + val limit = chapterListResponse["limit"].int + + var offset = chapterListResponse["offset"].int + + var hasMoreResults = (limit + offset) < chapterListResponse["total"].int + + // 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 newChapterListJson = JsonParser.parseString(newResponse.body!!.string()).obj + chapterListResults.addAll(newChapterListJson["results"].array.map { it.obj }) + hasMoreResults = (limit + offset) < newChapterListJson["total"].int } - chapterName.add(chapterJson["title"].string) + + val groupMap = helper.createGroupMap(chapterListResults.toList(), client) + + val now = Date().time + + return chapterListResults.map { helper.createChapter(it, groupMap) } + .filter { it.date_upload <= now && "MangaPlus" != it.scanlator } + } catch (e: Exception) { + Log.e("MangaDex", "error parsing chapter list", e) + throw(e) } - // if volume, chapter and title is empty its a oneshot - if (chapterName.isEmpty()) { - chapterName.add("Oneshot") - } - if ((status == 2 || status == 3) && doesFinalChapterExist( - finalChapterNumber, - chapterJson - ) - ) { - chapterName.add("[END]") - } - - chapter.name = cleanString(chapterName.joinToString(" ")) - // Convert from unix time - chapter.date_upload = chapterJson.get("timestamp").long * 1000 - val scanlatorName = chapterJson["groups"].asJsonArray.map { it.int }.map { - groups[it] - } - - chapter.scanlator = cleanString(scanlatorName.joinToString(" & ")) - - return chapter - } - - override fun chapterFromElement(element: Element) = throw Exception("Not used") - - override fun fetchPageList(chapter: SChapter): Observable> { - return client.newCall(pageListRequest(chapter)) - .asObservable().doOnNext { response -> - if (!response.isSuccessful) { - response.close() - if (response.code == 451) { - error("Error 451: Log in to view manga; contact MangaDex if error persists.") - } else { - throw Exception("HTTP error ${response.code}") - } - } - } - .map { response -> - pageListParse(response) - } } override fun pageListRequest(chapter: SChapter): Request { - if (chapter.scanlator == "MangaPlus") { - throw Exception("Chapter is licensed; use the MangaPlus extension") + if (!helper.containsUuid(chapter.url)) { + throw Exception("Migrate this manga from MangaDex to MangaDex to update it") } - - val server = getServer() - val saver = getUseDataSaver() - val newUrl = API_URL + chapter.url.replace(OLD_API_CHAPTER, NEW_API_CHAPTER) - return GET( - "$newUrl?server=$server&saver=$saver", - headers, - CacheControl.FORCE_NETWORK - ) + return GET(MDConstants.apiUrl + chapter.url, headers, CacheControl.FORCE_NETWORK) } - override fun pageListParse(document: Document) = throw Exception("Not used") - override fun pageListParse(response: Response): List { - val jsonData = response.body!!.string() - val json = JsonParser().parse(jsonData).asJsonObject["data"] + val chapterJson = JsonParser.parseString(response.body!!.string()).obj["data"] + val atHomeRequestUrl = "${MDConstants.apiUrl}/at-home/server/${chapterJson["id"].string}" - val hash = json["hash"].string - val server = json["server"].string + val host = + helper.getMdAtHomeUrl(atHomeRequestUrl, client, headers, CacheControl.FORCE_NETWORK) - return json["pages"].asJsonArray.mapIndexed { idx, it -> - val url = "$hash/${it.asString}" - val mdAtHomeMetadataUrl = "$server,${response.request.url},${Date().time}" - Page(idx, mdAtHomeMetadataUrl, url) + val usingDataSaver = preferences.getInt(MDConstants.dataSaverPref, 0) == 1 + + // have to add the time, and url to the page because pages timeout within 30mins now + val now = Date().time + val hash = chapterJson["attributes"]["hash"].string + val pageSuffix = if (usingDataSaver) { + chapterJson["attributes"]["dataSaver"].array.map { "/data-saver/$hash/${it.string}" } + } else { + chapterJson["attributes"]["data"].array.map { "/data/$hash/${it.string}" } + } + + return pageSuffix.mapIndexed { index, imgUrl -> + val mdAtHomeMetadataUrl = "$host,$atHomeRequestUrl,$now" + Page(index, mdAtHomeMetadataUrl, imgUrl) } } override fun imageRequest(page: Page): Request { - val url = when { - // Legacy - page.url.isEmpty() -> page.imageUrl!! - // Some images are hosted elsewhere - !page.url.startsWith("http") -> baseUrl + page.url.substringBefore(",") + page.imageUrl - // New chapters on MD servers - page.url.contains("https://mangadex.org/data") -> page.url.substringBefore(",") + page.imageUrl - // MD@Home token handling - else -> { - val tokenLifespan = 5 * 60 * 1000 - val data = page.url.split(",") - var tokenedServer = data[0] - if (Date().time - data[2].toLong() > tokenLifespan) { - val tokenRequestUrl = data[1] - val cacheControl = - if (Date().time - (tokenTracker[tokenRequestUrl] ?: 0) > tokenLifespan) { - tokenTracker[tokenRequestUrl] = Date().time - CacheControl.FORCE_NETWORK - } else { - CacheControl.FORCE_CACHE - } - val jsonData = - client.newCall(GET(tokenRequestUrl, headers, cacheControl)).execute() - .body!!.string() - tokenedServer = - JsonParser().parse(jsonData).asJsonObject["data"]["server"].string - } - tokenedServer + page.imageUrl - } - } - - return GET(url, headers) + return helper.getValidImageUrlForPage(page, headers, client) } - // chapter url where we get the token, last request time - private val tokenTracker = hashMapOf() - - override fun imageUrlParse(document: Document): String = "" - - private fun parseStatus(status: Int) = when (status) { - 1 -> SManga.ONGOING - else -> SManga.UNKNOWN - } + override fun imageUrlParse(response: Response): String = "" + // mangadex is mvp no settings yet override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { - val r18Pref = androidx.preference.ListPreference(screen.context).apply { - key = SHOW_R18_PREF_Title - title = SHOW_R18_PREF_Title - - title = SHOW_R18_PREF_Title - entries = arrayOf("Show No R18+", "Show All", "Show Only R18+") - entryValues = arrayOf("0", "1", "2") - summary = "%s" - - setOnPreferenceChangeListener { _, newValue -> - val selected = newValue as String - val index = this.findIndexOfValue(selected) - preferences.edit().putInt(SHOW_R18_PREF, index).commit() - } - } - val thumbsPref = androidx.preference.ListPreference(screen.context).apply { - key = SHOW_THUMBNAIL_PREF_Title - title = SHOW_THUMBNAIL_PREF_Title - entries = arrayOf("Show high quality", "Show low quality") - entryValues = arrayOf("0", "1") - summary = "%s" - - setOnPreferenceChangeListener { _, newValue -> - val selected = newValue as String - val index = this.findIndexOfValue(selected) - preferences.edit().putInt(SHOW_THUMBNAIL_PREF, index).commit() - } - } - val serverPref = androidx.preference.ListPreference(screen.context).apply { - key = SERVER_PREF_Title - title = SERVER_PREF_Title - entries = SERVER_PREF_ENTRIES - entryValues = SERVER_PREF_ENTRY_VALUES - summary = "%s" - - setOnPreferenceChangeListener { _, newValue -> - val selected = newValue as String - val index = this.findIndexOfValue(selected) - val entry = entryValues[index] as String - preferences.edit().putString(SERVER_PREF, entry).commit() - } - } val dataSaverPref = androidx.preference.ListPreference(screen.context).apply { - key = DATA_SAVER_PREF_Title - title = DATA_SAVER_PREF_Title + key = MDConstants.dataSaverPref + title = MDConstants.dataSaverPrefTitle entries = arrayOf("Disable", "Enable") entryValues = arrayOf("0", "1") summary = "%s" + setDefaultValue("0") setOnPreferenceChangeListener { _, newValue -> val selected = newValue as String val index = this.findIndexOfValue(selected) - preferences.edit().putInt(DATA_SAVER_PREF, index).commit() + preferences.edit().putInt(MDConstants.dataSaverPref, index).commit() } } - screen.addPreference(r18Pref) - screen.addPreference(thumbsPref) - screen.addPreference(serverPref) screen.addPreference(dataSaverPref) } - private fun getShowR18(): Int = preferences.getInt(SHOW_R18_PREF, 0) - private fun getShowThumbnail(): Int = preferences.getInt(SHOW_THUMBNAIL_PREF, 0) - private fun getServer(): String { - val default = SERVER_PREF_ENTRY_VALUES.first() - return preferences.getString(SERVER_PREF, default).takeIf { it in SERVER_PREF_ENTRY_VALUES } - ?: default - } - - private fun getUseDataSaver(): Int = preferences.getInt(DATA_SAVER_PREF, 0) - - private class TextField(name: String, val key: String) : Filter.Text(name) - private class Tag(val id: String, name: String) : Filter.TriState(name) - private class Demographic(demographics: List) : - Filter.Group("Demographic", demographics) - - private class PublicationStatus(publications: List) : - Filter.Group("Publication", publications) - - private class ContentList(contents: List) : Filter.Group("Content", contents) - private class FormatList(formats: List) : Filter.Group("Format", formats) - private class GenreList(genres: List) : Filter.Group("Genres", genres) - private class R18 : - Filter.Select("R18+", arrayOf("Default", "Show all", "Show only", "Show none")) - - private class ScanGroup(name: String) : Filter.Text(name) - - private fun getDemographic() = listOf( - Tag("1", "Shounen"), - Tag("2", "Shoujo"), - Tag("3", "Seinen"), - Tag("4", "Josei") - ).sortedWith(compareBy { it.name }) - - private fun getPublicationStatus() = listOf( - Tag("1", "Ongoing"), - Tag("2", "Completed"), - Tag("3", "Cancelled"), - Tag("4", "Hiatus") - ).sortedWith(compareBy { it.name }) - - private class ThemeList(themes: List) : Filter.Group("Themes", themes) - private class TagInclusionMode : - Filter.Select("Tag inclusion mode", arrayOf("All (and)", "Any (or)"), 0) - - private class TagExclusionMode : - Filter.Select("Tag exclusion mode", arrayOf("All (and)", "Any (or)"), 1) - - // default selection (Rating Descending) matches popularMangaRequest url - class SortFilter : Filter.Sort( - "Sort", - sortables.map { it.first }.toTypedArray(), - Selection(3, false) - ) - - private class OriginalLanguage : - Filter.Select("Original Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray()) - - override fun getFilterList() = FilterList( - TextField("Author", "author"), - TextField("Artist", "artist"), - R18(), - SortFilter(), - Demographic(getDemographic()), - PublicationStatus(getPublicationStatus()), - OriginalLanguage(), - ContentList(getContentList()), - FormatList(getFormatList()), - GenreList(getGenreList()), - ThemeList(getThemeList()), - TagInclusionMode(), - TagExclusionMode(), - Filter.Separator(), - Filter.Header("Group search ignores other inputs"), - ScanGroup("Search for manga by scanlator group") - ) - - private fun getContentList() = listOf( - Tag("9", "Ecchi"), - Tag("32", "Smut"), - Tag("49", "Gore"), - Tag("50", "Sexual Violence") - ).sortedWith(compareBy { it.name }) - - private fun getFormatList() = listOf( - Tag("1", "4-koma"), - Tag("4", "Award Winning"), - Tag("7", "Doujinshi"), - Tag("21", "Oneshot"), - Tag("36", "Long Strip"), - Tag("42", "Adaptation"), - Tag("43", "Anthology"), - Tag("44", "Web Comic"), - Tag("45", "Full Color"), - Tag("46", "User Created"), - Tag("47", "Official Colored"), - Tag("48", "Fan Colored") - ).sortedWith(compareBy { it.name }) - - private fun getGenreList() = listOf( - Tag("2", "Action"), - Tag("3", "Adventure"), - Tag("5", "Comedy"), - Tag("8", "Drama"), - Tag("10", "Fantasy"), - Tag("13", "Historical"), - Tag("14", "Horror"), - Tag("17", "Mecha"), - Tag("18", "Medical"), - Tag("20", "Mystery"), - Tag("22", "Psychological"), - Tag("23", "Romance"), - Tag("25", "Sci-Fi"), - Tag("28", "Shoujo Ai"), - Tag("30", "Shounen Ai"), - Tag("31", "Slice of Life"), - Tag("33", "Sports"), - Tag("35", "Tragedy"), - Tag("37", "Yaoi"), - Tag("38", "Yuri"), - Tag("41", "Isekai"), - Tag("51", "Crime"), - Tag("52", "Magical Girls"), - Tag("53", "Philosophical"), - Tag("54", "Superhero"), - Tag("55", "Thriller"), - Tag("56", "Wuxia") - ).sortedWith(compareBy { it.name }) - - private fun getThemeList() = listOf( - Tag("6", "Cooking"), - Tag("11", "Gyaru"), - Tag("12", "Harem"), - Tag("16", "Martial Arts"), - Tag("19", "Music"), - Tag("24", "School Life"), - Tag("34", "Supernatural"), - Tag("40", "Video Games"), - Tag("57", "Aliens"), - Tag("58", "Animals"), - Tag("59", "Crossdressing"), - Tag("60", "Demons"), - Tag("61", "Delinquents"), - Tag("62", "Genderswap"), - Tag("63", "Ghosts"), - Tag("64", "Monster Girls"), - Tag("65", "Loli"), - Tag("66", "Magic"), - Tag("67", "Military"), - Tag("68", "Monsters"), - Tag("69", "Ninja"), - Tag("70", "Office Workers"), - Tag("71", "Police"), - Tag("72", "Post-Apocalyptic"), - Tag("73", "Reincarnation"), - Tag("74", "Reverse Harem"), - Tag("75", "Samurai"), - Tag("76", "Shota"), - Tag("77", "Survival"), - Tag("78", "Time Travel"), - Tag("79", "Vampires"), - Tag("80", "Traditional Games"), - Tag("81", "Virtual Reality"), - Tag("82", "Zombies"), - Tag("83", "Incest"), - Tag("84", "Mafia"), - Tag("85", "Villainess") - ).sortedWith(compareBy { it.name }) - - private val GENRES = - (getContentList() + getFormatList() + getGenreList() + getThemeList()).map { it.id to it.name } - .toMap() - - companion object { - private val WHITESPACE_REGEX = "\\s".toRegex() - - // This number matches to the cookie - private const val NO_R18 = 0 - private const val ALL = 1 - private const val ONLY_R18 = 2 - - private const val SHOW_R18_PREF_Title = "Default R18 Setting" - private const val SHOW_R18_PREF = "showR18Default" - - private const val LOW_QUALITY = 1 - - private const val SHOW_THUMBNAIL_PREF_Title = "Default thumbnail quality" - private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault" - - private const val SERVER_PREF_Title = "Image server" - private const val SERVER_PREF = "imageServer" - private val SERVER_PREF_ENTRIES = - arrayOf("Automatic", "NA/EU 1", "NA/EU 2", "Rest of the world") - private val SERVER_PREF_ENTRY_VALUES = arrayOf("0", "na", "na2", "row") - - private const val DATA_SAVER_PREF_Title = "Data saver" - private const val DATA_SAVER_PREF = "dataSaver" - - private const val API_URL = "https://api.mangadex.org" - private const val API_MANGA = "/v2/manga/" - private const val API_MANGA_INCLUDE_CHAPTERS = "?include=chapters" - private const val OLD_API_CHAPTER = "/api/chapter/" - private const val NEW_API_CHAPTER = "/v2/chapter/" - - const val PREFIX_ID_SEARCH = "id:" - - private val sortables = listOf( - Triple("Update date", 0, 1), - Triple("Alphabetically", 2, 3), - Triple("Number of comments", 4, 5), - Triple("Rating", 6, 7), - Triple("Views", 8, 9), - Triple("Follows", 10, 11) - ) - - private val SOURCE_LANG_LIST = listOf( - Pair("All", "0"), - Pair("Japanese", "2"), - Pair("English", "1"), - Pair("Polish", "3"), - Pair("German", "8"), - Pair("French", "10"), - Pair("Vietnamese", "12"), - Pair("Chinese", "21"), - Pair("Indonesian", "27"), - Pair("Korean", "28"), - Pair("Spanish (LATAM)", "29"), - Pair("Thai", "32"), - Pair("Filipino", "34") - ) - } -} - -class CoverInterceptor : Interceptor { - private val coverRegex = Regex("""/images/.*\.jpg""") - - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - - return chain.proceed(chain.request()).let { response -> - if (response.code == 404 && originalRequest.url.toString().contains(coverRegex)) { - response.close() - chain.proceed( - originalRequest.newBuilder().url( - originalRequest.url.toString().substringBeforeLast(".") + ".thumb.jpg" - ).build() - ) - } else { - response - } - } - } -} - -class MdRateLimitInterceptor : Interceptor { - private val coverRegex = Regex("""/images/.*\.jpg""") - private val baseInterceptor = RateLimitInterceptor(2) - - override fun intercept(chain: Interceptor.Chain): Response = - if (chain.request().url.toString().contains(coverRegex)) - chain.proceed(chain.request()) - else - baseInterceptor.intercept(chain) -} - -class MdAtHomeReportInterceptor( - private val client: OkHttpClient, - private val headers: Headers -) : Interceptor { - - private val gson: Gson by lazy { Gson() } - private val mdAtHomeUrlRegex = Regex("""^https://[\w\d]+\.[\w\d]+\.mangadex\.network.*${'$'}""") - - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - - return chain.proceed(chain.request()).let { response -> - val url = originalRequest.url.toString() - if (url.contains(mdAtHomeUrlRegex)) { - val jsonString = gson.toJson( - mapOf( - "url" to url, - "success" to response.isSuccessful, - "bytes" to response.peekBody(Long.MAX_VALUE).bytes().size - ) - ) - - val postResult = client.newCall( - POST( - "https://api.mangadex.network/report", - headers, - RequestBody.create(null, jsonString) - ) - ) - try { - postResult.execute() - } catch (e: Exception) { - Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}") - } - } - - response - } - } + override fun getFilterList(): FilterList = helper.mdFilters.getMDFilterList() } diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt index 0967ca916..0129590a6 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt @@ -51,45 +51,46 @@ class MangaDexFactory : SourceFactory { MangaDexOther() ) } -class MangaDexEnglish : MangaDex("en", "gb") -class MangaDexJapanese : MangaDex("ja", "jp") -class MangaDexPolish : MangaDex("pl", "pl") -class MangaDexSerboCroatian : MangaDex("sh", "rs") -class MangaDexDutch : MangaDex("nl", "nl") -class MangaDexItalian : MangaDex("it", "it") -class MangaDexRussian : MangaDex("ru", "ru") -class MangaDexGerman : MangaDex("de", "de") -class MangaDexHungarian : MangaDex("hu", "hu") -class MangaDexFrench : MangaDex("fr", "fr") -class MangaDexFinnish : MangaDex("fi", "fi") -class MangaDexVietnamese : MangaDex("vi", "vn") -class MangaDexGreek : MangaDex("el", "gr") -class MangaDexBulgarian : MangaDex("bg", "bg") -class MangaDexSpanishSpain : MangaDex("es", "es") -class MangaDexPortugueseBrazil : MangaDex("pt-BR", "br") -class MangaDexPortuguesePortugal : MangaDex("pt", "pt") -class MangaDexSwedish : MangaDex("sv", "se") -class MangaDexArabic : MangaDex("ar", "sa") -class MangaDexDanish : MangaDex("da", "dk") -class MangaDexChineseSimp : MangaDex("zh-Hans", "cn") -class MangaDexBengali : MangaDex("bn", "bd") -class MangaDexRomanian : MangaDex("ro", "ro") -class MangaDexCzech : MangaDex("cs", "cz") -class MangaDexMongolian : MangaDex("mn", "mn") -class MangaDexTurkish : MangaDex("tr", "tr") -class MangaDexIndonesian : MangaDex("id", "id") -class MangaDexKorean : MangaDex("ko", "kr") -class MangaDexSpanishLTAM : MangaDex("es-419", "mx") -class MangaDexPersian : MangaDex("fa", "ir") -class MangaDexMalay : MangaDex("ms", "my") -class MangaDexThai : MangaDex("th", "th") -class MangaDexCatalan : MangaDex("ca", "ct") -class MangaDexFilipino : MangaDex("fil", "ph") -class MangaDexChineseTrad : MangaDex("zh-Hant", "hk") -class MangaDexUkrainian : MangaDex("uk", "ua") -class MangaDexBurmese : MangaDex("my", "mm") -class MangaDexLithuanian : MangaDex("lt", "il") -class MangaDexHebrew : MangaDex("he", "il") -class MangaDexHindi : MangaDex("hi", "in") -class MangaDexNorwegian : MangaDex("no", "no") -class MangaDexOther : MangaDex("other", " ") + +class MangaDexEnglish : MangaDex("en") +class MangaDexJapanese : MangaDex("ja") +class MangaDexPolish : MangaDex("pl") +class MangaDexSerboCroatian : MangaDex("sh") +class MangaDexDutch : MangaDex("nl") +class MangaDexItalian : MangaDex("it") +class MangaDexRussian : MangaDex("ru") +class MangaDexGerman : MangaDex("de") +class MangaDexHungarian : MangaDex("hu") +class MangaDexFrench : MangaDex("fr") +class MangaDexFinnish : MangaDex("fi") +class MangaDexVietnamese : MangaDex("vi") +class MangaDexGreek : MangaDex("el") +class MangaDexBulgarian : MangaDex("bg") +class MangaDexSpanishSpain : MangaDex("es") +class MangaDexPortugueseBrazil : MangaDex("pt-BR") +class MangaDexPortuguesePortugal : MangaDex("pt") +class MangaDexSwedish : MangaDex("sv") +class MangaDexArabic : MangaDex("ar") +class MangaDexDanish : MangaDex("da") +class MangaDexChineseSimp : MangaDex("zh-Hans") +class MangaDexBengali : MangaDex("bn") +class MangaDexRomanian : MangaDex("ro") +class MangaDexCzech : MangaDex("cs") +class MangaDexMongolian : MangaDex("mn") +class MangaDexTurkish : MangaDex("tr") +class MangaDexIndonesian : MangaDex("id") +class MangaDexKorean : MangaDex("ko") +class MangaDexSpanishLTAM : MangaDex("es-419") +class MangaDexPersian : MangaDex("fa") +class MangaDexMalay : MangaDex("ms") +class MangaDexThai : MangaDex("th") +class MangaDexCatalan : MangaDex("ca") +class MangaDexFilipino : MangaDex("fil") +class MangaDexChineseTrad : MangaDex("zh-Hant") +class MangaDexUkrainian : MangaDex("uk") +class MangaDexBurmese : MangaDex("my") +class MangaDexLithuanian : MangaDex("lt") +class MangaDexHebrew : MangaDex("he") +class MangaDexHindi : MangaDex("hi") +class MangaDexNorwegian : MangaDex("no") +class MangaDexOther : MangaDex("other") 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 new file mode 100644 index 000000000..0555568b3 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt @@ -0,0 +1,249 @@ +package eu.kanade.tachiyomi.extension.all.mangadex + +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() = FilterList( + OriginalLanguageList(getOriginalLanguage()), + ContentRatingList(getContentRating()), + DemographicList(getDemographics()), + StatusList(getStatus()), + SortFilter(sortableList.map { it.first }.toTypedArray()), + TagList(getTags()), + TagInclusionMode(), + TagExclusionMode(), + ) + + private class Demographic(name: String) : Filter.CheckBox(name) + private class DemographicList(demographics: List) : + Filter.Group("Publication Demographic", demographics) + + private fun getDemographics() = listOf( + Demographic("None"), + Demographic("Shounen"), + Demographic("Shoujo"), + Demographic("Seinen"), + Demographic("Josei") + ) + + private class Status(name: String) : Filter.CheckBox(name) + private class StatusList(status: List) : + Filter.Group("Status", status) + + private fun getStatus() = listOf( + Status("Onging"), + Status("Completed"), + Status("Hiatus"), + Status("Abandoned"), + ) + + private class ContentRating(name: String) : Filter.CheckBox(name) + private class ContentRatingList(contentRating: List) : + Filter.Group("Content Rating", contentRating) + + private fun getContentRating() = listOf( + ContentRating("Safe"), + ContentRating("Suggestive"), + ContentRating("Erotica"), + ContentRating("Pornographic") + ) + + private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name) + private class OriginalLanguageList(originalLanguage: List) : + Filter.Group("Original language", originalLanguage) + + private fun getOriginalLanguage() = listOf( + OriginalLanguage("Japanese (Manga)", "jp"), + OriginalLanguage("Chinese (Manhua)", "cn"), + OriginalLanguage("Korean (Manhwa)", "kr"), + ) + + internal class Tag(val id: String, name: String) : Filter.TriState(name) + private class TagList(tags: List) : Filter.Group("Tags", tags) + + 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 Love"), + Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"), + Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"), + Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"), + Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "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("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Ecchi"), + 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 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("t36fd93ea-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 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("Default (Asc/Desc doesn't matter)", ""), + Pair("Created at", "createdAt"), + Pair("Updated at", "updatedAt"), + ) + + class SortFilter(sortables: Array) : Filter.Sort("Sort", sortables, Selection(0, false)) + + internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList): String { + url.apply { + // add filters + filters.forEach { filter -> + when (filter) { + is OriginalLanguageList -> { + filter.state.forEach { lang -> + if (lang.state) { + 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) { + if (filter.state!!.index != 0) { + 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) + ) + } + } + } + } + return url.toString() + } +} 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 new file mode 100644 index 000000000..450f20610 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt @@ -0,0 +1,278 @@ +package eu.kanade.tachiyomi.extension.all.mangadex + +import android.util.Log +import com.github.salomonbrys.kotson.array +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.nullString +import com.github.salomonbrys.kotson.obj +import com.github.salomonbrys.kotson.string +import com.google.gson.JsonElement +import com.google.gson.JsonParser +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 okhttp3.CacheControl +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.parser.Parser +import java.util.Date + +class MangaDexHelper() { + + val mdFilters = MangaDexFilters() + + /** + * Gets the UUID from the url + */ + fun getUUIDFromUrl(url: String) = url.substringAfterLast("/") + + /** + * get the manga feed url + */ + fun getChapterEndpoint(mangaId: String, offset: Int, langCode: String) = + "${MDConstants.apiMangaUrl}/$mangaId/feed?limit=500&offset=$offset&locales[]=$langCode" + + /** + * Check if the manga id is a valid uuid + */ + fun containsUuid(id: String) = id.contains(MDConstants.uuidRegex) + + /** + * Get the manga offset pages are 1 based, so subtract 1 + */ + fun getMangaListOffset(page: Int): String = (MDConstants.mangaLimit * (page - 1)).toString() + + /** + * Remove bbcode tags as well as parses any html characters in description or + * chapter name to actual characters for example ♥ will show ♥ + */ + fun cleanString(string: String): String { + val bbRegex = + """\[(\w+)[^]]*](.*?)\[/\1]""".toRegex() + var intermediate = string + .replace("[list]", "") + .replace("[/list]", "") + .replace("[*]", "") + // Recursively remove nested bbcode + while (bbRegex.containsMatchIn(intermediate)) { + intermediate = intermediate.replace(bbRegex, "$2") + } + return Parser.unescapeEntities(intermediate, false) + } + + /**Maps dex status to tachi status + * abandoned and completed statuses's need addition checks with chapter info if we are to be accurate + */ + fun getPublicationStatus(dexStatus: String?): Int { + return when (dexStatus) { + null -> SManga.UNKNOWN + "ongoing" -> SManga.ONGOING + "hiatus" -> SManga.ONGOING + else -> SManga.UNKNOWN + } + } + + fun parseDate(dateAsString: String): Long = + MDConstants.dateFormatter.parse(dateAsString)?.time ?: 0 + + // chapter url where we get the token, last request time + private val tokenTracker = hashMapOf() + + // Check the token map to see if the md@home host is still valid + fun getValidImageUrlForPage(page: Page, headers: Headers, client: OkHttpClient): Request { + val data = page.url.split(",") + val mdAtHomeServerUrl = + when (Date().time - data[2].toLong() > MDConstants.mdAtHomeTokenLifespan) { + false -> data[0] + true -> { + val tokenRequestUrl = data[1] + val cacheControl = + if (Date().time - ( + tokenTracker[tokenRequestUrl] + ?: 0 + ) > MDConstants.mdAtHomeTokenLifespan + ) { + tokenTracker[tokenRequestUrl] = Date().time + CacheControl.FORCE_NETWORK + } else { + CacheControl.FORCE_CACHE + } + getMdAtHomeUrl(tokenRequestUrl, client, headers, cacheControl) + } + } + return GET(mdAtHomeServerUrl + page.imageUrl, headers) + } + + /** + * get the md@home url + */ + fun getMdAtHomeUrl( + tokenRequestUrl: String, + client: OkHttpClient, + headers: Headers, + cacheControl: CacheControl + ): String { + val response = + client.newCall(GET(tokenRequestUrl, headers, cacheControl)).execute() + return JsonParser.parseString(response.body!!.string()).obj["baseUrl"].string + } + + /** + * create an SManga from json element only basic elements + */ + fun createManga(mangaJson: JsonElement): SManga { + val data = mangaJson["data"].obj + val dexId = data["id"].string + val attr = data["attributes"].obj + + return SManga.create().apply { + url = "/manga/$dexId" + title = cleanString(attr["title"]["en"].string) + thumbnail_url = "" + } + } + + /** + * Create an SManga from json element with all details + */ + fun createManga(mangaJson: JsonElement, client: OkHttpClient): SManga { + try { + val data = mangaJson["data"].obj + val dexId = data["id"].string + val attr = data["attributes"].obj + + // things that will go with the genre tags but aren't actually genre + val nonGenres = listOf( + attr["contentRating"].nullString, + attr["originalLanguage"]?.nullString, + attr["publicationDemographic"]?.nullString + ) + + // get authors ignore if they error, artists are labelled as authors currently + val authorIds = mangaJson["relationships"].array.filter { relationship -> + relationship["type"].string.equals("author", true) + }.map { relationship -> relationship["id"].string } + .distinct() + + val authors = runCatching { + val ids = authorIds.joinToString("&ids[]=", "?ids[]=") + val response = client.newCall(GET("${MDConstants.apiUrl}/author$ids")).execute() + val json = JsonParser.parseString(response.body!!.string()) + json.obj["results"].array.map { result -> + cleanString(result["data"]["attributes"]["name"].string) + } + }.getOrNull() ?: emptyList() + + // get tag list + val tags = mdFilters.getTags() + + // map ids to tag names + val genreList = ( + attr["tags"].array + .map { it["id"].string } + .map { dexTag -> + tags.firstOrNull { it.name.equals(dexTag, true) } + }.map { it?.name } + + nonGenres + ) + .filterNotNull() + + return SManga.create().apply { + url = "/manga/$dexId" + title = cleanString(attr["title"]["en"].string) + description = cleanString(attr["description"]["en"].string) + author = authors.joinToString(", ") + status = getPublicationStatus(attr["publicationDemographic"].nullString) + thumbnail_url = "" + genre = genreList.joinToString(", ") + } + } catch (e: Exception) { + Log.e("MangaDex", "error parsing manga", e) + throw(e) + } + } + + /** + * This makes an api call per a unique group id found in the chapters hopefully Dex will eventually support + * batch ids + */ + fun createGroupMap( + chapterListResults: List, + client: OkHttpClient + ): Map { + val groupIds = + chapterListResults.map { it["relationships"].array } + .flatten() + .filter { it["type"].string == "scanlation_group" } + .map { it["id"].string }.distinct() + + // ignore errors if request fails, there is no batch group search yet.. + return runCatching { + groupIds.chunked(100).map { chunkIds -> + val ids = chunkIds.joinToString("&ids[]=", "?ids[]=") + val groupResponse = + client.newCall(GET("${MDConstants.apiUrl}/group$ids")).execute() + // map results to pair id and name + JsonParser.parseString(groupResponse.body!!.string()) + .obj["results"].array.map { result -> + val id = result["data"]["id"].string + val name = result["data"]["attributes"]["name"].string + Pair(id, cleanString(name)) + } + }.flatten().toMap() + }.getOrNull() ?: emptyMap() + } + + /** + * create the SChapter from json + */ + fun createChapter(chapterJsonResponse: JsonElement, groupMap: Map): SChapter { + try { + val data = chapterJsonResponse["data"].obj + val scanlatorGroupIds = + chapterJsonResponse["relationships"].array.filter { it["type"].string == "scanlation_group" } + .map { groupMap[it["id"].string] } + .joinToString(" & ") + val attr = data["attributes"] + + val chapterName = mutableListOf() + // Build chapter name + + attr["volume"].nullString?.let { + if (it.isNotEmpty()) { + chapterName.add("Vol.$it") + } + } + + attr["chapter"].nullString?.let { + if (it.isNotEmpty()) { + chapterName.add("Ch.$it") + } + } + + attr["title"].nullString?.let { + if (chapterName.isNotEmpty() && it.isNotEmpty()) { + chapterName.add("-") + chapterName.add(it) + } + } + // if volume, chapter and title is empty its a oneshot + if (chapterName.isEmpty()) { + chapterName.add("Oneshot") + } + // In future calculate [END] if non mvp api doesnt provide it + + return SChapter.create().apply { + url = "/chapter/${data["id"].string}" + name = cleanString(chapterName.joinToString(" ")) + date_upload = parseDate(attr["publishAt"].string) + scanlator = scanlatorGroupIds + } + } catch (e: Exception) { + Log.e("MangaDex", "error parsing chapter", e) + throw(e) + } + } +} diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexInterceptors.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexInterceptors.kt new file mode 100644 index 000000000..c06384a97 --- /dev/null +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexInterceptors.kt @@ -0,0 +1,90 @@ +package eu.kanade.tachiyomi.extension.all.mangadex + +import android.util.Log +import com.google.gson.Gson +import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor +import eu.kanade.tachiyomi.network.POST +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okhttp3.Response +import okhttp3.internal.closeQuietly + +/** + * Rate limit requests ignore covers though + */ + +private val coverRegex = Regex("""/images/.*\.jpg""") +private val baseInterceptor = RateLimitInterceptor(3) + +val mdRateLimitInterceptor = Interceptor { chain -> + return@Interceptor when (chain.request().url.toString().contains(coverRegex)) { + true -> chain.proceed(chain.request()) + false -> baseInterceptor.intercept(chain) + } +} + +/** + * Interceptor to post to md@home for MangaDex Stats + */ +class MdAtHomeReportInterceptor( + private val client: OkHttpClient, + private val headers: Headers +) : Interceptor { + + private val gson: Gson by lazy { Gson() } + private val mdAtHomeUrlRegex = + Regex("""^https://[\w\d]+\.[\w\d]+\.mangadex(\b-test\b)?\.network.*${'$'}""") + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + return chain.proceed(chain.request()).let { response -> + val url = originalRequest.url.toString() + if (url.contains(mdAtHomeUrlRegex)) { + val jsonString = gson.toJson( + mapOf( + "url" to url, + "success" to response.isSuccessful, + "bytes" to response.peekBody(Long.MAX_VALUE).bytes().size + ) + ) + + val postResult = client.newCall( + POST( + MDConstants.atHomePostUrl, + headers, + RequestBody.create(null, jsonString) + ) + ) + try { + val body = postResult.execute() + body.closeQuietly() + } catch (e: Exception) { + Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}") + } + } + + response + } + } +} + +val coverInterceptor = Interceptor { chain -> + val originalRequest = chain.request() + return@Interceptor chain.proceed(chain.request()).let { response -> + if (response.code == 404 && originalRequest.url.toString() + .contains(coverRegex) + ) { + response.close() + chain.proceed( + originalRequest.newBuilder().url( + originalRequest.url.toString().substringBeforeLast(".") + ".thumb.jpg" + ).build() + ) + } else { + response + } + } +} diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexDescription.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexDescription.kt deleted file mode 100644 index 2ce0a8e6c..000000000 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexDescription.kt +++ /dev/null @@ -1,54 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.mangadex - -class MangadexDescription(internalLang: String) { - - private val listOfLangs = when (internalLang) { - "ru" -> RUSSIAN - "de" -> GERMAN - "it" -> ITALIAN - in "es", "mx" -> SPANISH - in "br", "pt" -> PORTUGESE - "tr" -> TURKISH - "fr" -> FRENCH - "sa" -> ARABIC - else -> emptyList() - } - - fun clean(description: String): String { - val langList = ALL_LANGS.toMutableList() - - // remove any languages before the ones provided in the langTextToCheck, if no matches or empty - // just uses the original description, also removes the potential lang from all lang list - var newDescription = description - listOfLangs.forEach { - newDescription = newDescription.substringAfter(it) - langList.remove(it) - } - - // remove any possible languages that remain to get the new description - langList.forEach { newDescription = newDescription.substringBefore(it) } - return newDescription - } - - companion object { - val ARABIC = listOf("[b][u]Arabic / العربية[/u][/b]") - val FRENCH = listOf( - "French - Français:", - "[b][u]French[/u][/b]", - "[b][u]French / Français[/u][/b]" - ) - val GERMAN = listOf("[b][u]German / Deutsch[/u][/b]", "German/Deutsch:") - val ITALIAN = listOf("[b][u]Italian / Italiano[/u][/b]") - val PORTUGESE = listOf( - "[b][u]Portuguese (BR) / Português (BR)[/u][/b]", - "[b][u]Português / Portuguese[/u][/b]", - "[b][u]Portuguese / Portugu[/u][/b]" - ) - val RUSSIAN = listOf("[b][u]Russian / Русский[/u][/b]") - val SPANISH = listOf("[b][u]Español / Spanish:[/u][/b]") - val TURKISH = listOf("[b][u]Turkish / Türkçe[/u][/b]") - - val ALL_LANGS = - listOf(ARABIC, FRENCH, GERMAN, ITALIAN, PORTUGESE, RUSSIAN, SPANISH, TURKISH).flatten() - } -} diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexUrlActivity.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexUrlActivity.kt index 5db4b10b1..0956e3719 100644 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexUrlActivity.kt +++ b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangadexUrlActivity.kt @@ -25,7 +25,7 @@ class MangadexUrlActivity : Activity() { val titleid = pathSegments[1] val mainIntent = Intent().apply { action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${MangaDex.PREFIX_ID_SEARCH}$titleid") + putExtra("query", "${MDConstants.prefixIdSearch}$titleid") putExtra("filter", packageName) }