Dex V5 (note: their are no covers currently, you will need to migrate from dex to dex) (#6843)

* initial v5 stuff

* more v5 stuff

* slight changes

* add search

* clean up search some

* change athome parsing
clean up filters

* add Status options for search

* update to use batch author endpoint

* add more filters

* small fixes

* more fixes

* change error message
This commit is contained in:
Carlos 2021-05-05 19:55:50 -04:00 committed by GitHub
parent f438e31b5d
commit 715bdcccc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 837 additions and 1051 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'MangaDex'
pkgNameSuffix = 'all.mangadex'
extClass = '.MangaDexFactory'
extVersionCode = 105
extVersionCode = 106
libVersion = '1.2'
containsNsfw = true
}

View File

@ -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
}
}

View File

@ -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")

View File

@ -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<Demographic>) :
Filter.Group<Demographic>("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<Status>) :
Filter.Group<Status>("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<ContentRating>) :
Filter.Group<ContentRating>("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<OriginalLanguage>) :
Filter.Group<OriginalLanguage>("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<Tag>) : Filter.Group<Tag>("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<String>("Included tags mode", arrayOf("And", "Or"), 0)
private class TagExclusionMode :
Filter.Select<String>("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<String>) : 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()
}
}

View File

@ -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 &hearts; 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<String, Long>()
// 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<JsonElement>,
client: OkHttpClient
): Map<String, String> {
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<String, String>): 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<String>()
// 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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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&ccedil;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&ecirc;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&ntilde;ol / Spanish:[/u][/b]")
val TURKISH = listOf("[b][u]Turkish / T&uuml;rk&ccedil;e[/u][/b]")
val ALL_LANGS =
listOf(ARABIC, FRENCH, GERMAN, ITALIAN, PORTUGESE, RUSSIAN, SPANISH, TURKISH).flatten()
}
}

View File

@ -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)
}