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:
parent
f438e31b5d
commit
715bdcccc1
|
@ -5,7 +5,7 @@ ext {
|
||||||
extName = 'MangaDex'
|
extName = 'MangaDex'
|
||||||
pkgNameSuffix = 'all.mangadex'
|
pkgNameSuffix = 'all.mangadex'
|
||||||
extClass = '.MangaDexFactory'
|
extClass = '.MangaDexFactory'
|
||||||
extVersionCode = 105
|
extVersionCode = 106
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
containsNsfw = true
|
containsNsfw = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -51,45 +51,46 @@ class MangaDexFactory : SourceFactory {
|
||||||
MangaDexOther()
|
MangaDexOther()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
class MangaDexEnglish : MangaDex("en", "gb")
|
|
||||||
class MangaDexJapanese : MangaDex("ja", "jp")
|
class MangaDexEnglish : MangaDex("en")
|
||||||
class MangaDexPolish : MangaDex("pl", "pl")
|
class MangaDexJapanese : MangaDex("ja")
|
||||||
class MangaDexSerboCroatian : MangaDex("sh", "rs")
|
class MangaDexPolish : MangaDex("pl")
|
||||||
class MangaDexDutch : MangaDex("nl", "nl")
|
class MangaDexSerboCroatian : MangaDex("sh")
|
||||||
class MangaDexItalian : MangaDex("it", "it")
|
class MangaDexDutch : MangaDex("nl")
|
||||||
class MangaDexRussian : MangaDex("ru", "ru")
|
class MangaDexItalian : MangaDex("it")
|
||||||
class MangaDexGerman : MangaDex("de", "de")
|
class MangaDexRussian : MangaDex("ru")
|
||||||
class MangaDexHungarian : MangaDex("hu", "hu")
|
class MangaDexGerman : MangaDex("de")
|
||||||
class MangaDexFrench : MangaDex("fr", "fr")
|
class MangaDexHungarian : MangaDex("hu")
|
||||||
class MangaDexFinnish : MangaDex("fi", "fi")
|
class MangaDexFrench : MangaDex("fr")
|
||||||
class MangaDexVietnamese : MangaDex("vi", "vn")
|
class MangaDexFinnish : MangaDex("fi")
|
||||||
class MangaDexGreek : MangaDex("el", "gr")
|
class MangaDexVietnamese : MangaDex("vi")
|
||||||
class MangaDexBulgarian : MangaDex("bg", "bg")
|
class MangaDexGreek : MangaDex("el")
|
||||||
class MangaDexSpanishSpain : MangaDex("es", "es")
|
class MangaDexBulgarian : MangaDex("bg")
|
||||||
class MangaDexPortugueseBrazil : MangaDex("pt-BR", "br")
|
class MangaDexSpanishSpain : MangaDex("es")
|
||||||
class MangaDexPortuguesePortugal : MangaDex("pt", "pt")
|
class MangaDexPortugueseBrazil : MangaDex("pt-BR")
|
||||||
class MangaDexSwedish : MangaDex("sv", "se")
|
class MangaDexPortuguesePortugal : MangaDex("pt")
|
||||||
class MangaDexArabic : MangaDex("ar", "sa")
|
class MangaDexSwedish : MangaDex("sv")
|
||||||
class MangaDexDanish : MangaDex("da", "dk")
|
class MangaDexArabic : MangaDex("ar")
|
||||||
class MangaDexChineseSimp : MangaDex("zh-Hans", "cn")
|
class MangaDexDanish : MangaDex("da")
|
||||||
class MangaDexBengali : MangaDex("bn", "bd")
|
class MangaDexChineseSimp : MangaDex("zh-Hans")
|
||||||
class MangaDexRomanian : MangaDex("ro", "ro")
|
class MangaDexBengali : MangaDex("bn")
|
||||||
class MangaDexCzech : MangaDex("cs", "cz")
|
class MangaDexRomanian : MangaDex("ro")
|
||||||
class MangaDexMongolian : MangaDex("mn", "mn")
|
class MangaDexCzech : MangaDex("cs")
|
||||||
class MangaDexTurkish : MangaDex("tr", "tr")
|
class MangaDexMongolian : MangaDex("mn")
|
||||||
class MangaDexIndonesian : MangaDex("id", "id")
|
class MangaDexTurkish : MangaDex("tr")
|
||||||
class MangaDexKorean : MangaDex("ko", "kr")
|
class MangaDexIndonesian : MangaDex("id")
|
||||||
class MangaDexSpanishLTAM : MangaDex("es-419", "mx")
|
class MangaDexKorean : MangaDex("ko")
|
||||||
class MangaDexPersian : MangaDex("fa", "ir")
|
class MangaDexSpanishLTAM : MangaDex("es-419")
|
||||||
class MangaDexMalay : MangaDex("ms", "my")
|
class MangaDexPersian : MangaDex("fa")
|
||||||
class MangaDexThai : MangaDex("th", "th")
|
class MangaDexMalay : MangaDex("ms")
|
||||||
class MangaDexCatalan : MangaDex("ca", "ct")
|
class MangaDexThai : MangaDex("th")
|
||||||
class MangaDexFilipino : MangaDex("fil", "ph")
|
class MangaDexCatalan : MangaDex("ca")
|
||||||
class MangaDexChineseTrad : MangaDex("zh-Hant", "hk")
|
class MangaDexFilipino : MangaDex("fil")
|
||||||
class MangaDexUkrainian : MangaDex("uk", "ua")
|
class MangaDexChineseTrad : MangaDex("zh-Hant")
|
||||||
class MangaDexBurmese : MangaDex("my", "mm")
|
class MangaDexUkrainian : MangaDex("uk")
|
||||||
class MangaDexLithuanian : MangaDex("lt", "il")
|
class MangaDexBurmese : MangaDex("my")
|
||||||
class MangaDexHebrew : MangaDex("he", "il")
|
class MangaDexLithuanian : MangaDex("lt")
|
||||||
class MangaDexHindi : MangaDex("hi", "in")
|
class MangaDexHebrew : MangaDex("he")
|
||||||
class MangaDexNorwegian : MangaDex("no", "no")
|
class MangaDexHindi : MangaDex("hi")
|
||||||
class MangaDexOther : MangaDex("other", " ")
|
class MangaDexNorwegian : MangaDex("no")
|
||||||
|
class MangaDexOther : MangaDex("other")
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ class MangadexUrlActivity : Activity() {
|
||||||
val titleid = pathSegments[1]
|
val titleid = pathSegments[1]
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${MangaDex.PREFIX_ID_SEARCH}$titleid")
|
putExtra("query", "${MDConstants.prefixIdSearch}$titleid")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue