MangaMutiny: WebView + pagination fix (#5900)

- WebView works (main page + pages of individual manga)
- changed baseUrl to https://mangamutiny.org and moved https://api.mangamutiny.org to different variable
(the extension still relies on the lightweight json responses from the API for everything that isn't WebView related - the baseUrl only had to be changed for WebView to work)
- fixed pagination bug (would previously skip 21 manga after the first 21 manga, now fixed)
- Internally reworked filters to make them compatible with the WebView changes (does not affect users in any way)
This commit is contained in:
E3FxGaming 2021-03-03 14:56:19 +01:00 committed by GitHub
parent 03d9353432
commit 3f6e313b87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 214 additions and 154 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'Manga Mutiny' extName = 'Manga Mutiny'
pkgNameSuffix = "en.mangamutiny" pkgNameSuffix = "en.mangamutiny"
extClass = '.MangaMutiny' extClass = '.MangaMutiny'
extVersionCode = 4 extVersionCode = 5
libVersion = '1.2' libVersion = '1.2'
containsNsfw = true containsNsfw = true
} }

View File

@ -36,13 +36,19 @@ fun JsonObject.getNullable(key: String): JsonElement? {
class MangaMutiny : HttpSource() { class MangaMutiny : HttpSource() {
override val name = "Manga Mutiny" override val name = "Manga Mutiny"
override val baseUrl = "https://api.mangamutiny.org" override val baseUrl = "https://mangamutiny.org"
override val supportsLatest = true override val supportsLatest = true
override val lang = "en" override val lang = "en"
private val parser = JsonParser() private val parser = JsonParser()
private val baseUrlAPI = "https://api.mangamutiny.org"
private val webViewSingleMangaPath = "title/"
private val webViewMultipleMangaPath = "titles/"
override fun headersBuilder(): Headers.Builder { override fun headersBuilder(): Headers.Builder {
return super.headersBuilder().apply { return super.headersBuilder().apply {
add("Accept", "application/json") add("Accept", "application/json")
@ -129,11 +135,17 @@ class MangaMutiny : HttpSource() {
override fun mangaDetailsRequest(manga: SManga): Request = mangaDetailsRequestCommon(manga) override fun mangaDetailsRequest(manga: SManga): Request = mangaDetailsRequestCommon(manga)
private fun mangaDetailsRequestCommon(manga: SManga, lite: Boolean = true): Request { private fun mangaDetailsRequestCommon(manga: SManga, lite: Boolean = true): Request {
val uri = Uri.parse(baseUrl).buildUpon() val uri = if (isForWebView()) {
.appendEncodedPath(apiMangaUrlPath) Uri.parse(baseUrl).buildUpon()
.appendPath(manga.url) .appendEncodedPath(webViewSingleMangaPath)
.appendPath(manga.url)
if (lite) uri.appendQueryParameter("lite", "1") } else {
Uri.parse(baseUrlAPI).buildUpon()
.appendEncodedPath(apiMangaUrlPath)
.appendPath(manga.url).let {
if (lite) it.appendQueryParameter("lite", "1") else it
}
}
return GET(uri.build().toString(), headers) return GET(uri.build().toString(), headers)
} }
@ -166,7 +178,7 @@ class MangaMutiny : HttpSource() {
} }
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val uri = Uri.parse(baseUrl).buildUpon() val uri = Uri.parse(baseUrlAPI).buildUpon()
.appendEncodedPath(apiChapterUrlPath) .appendEncodedPath(apiChapterUrlPath)
.appendEncodedPath(chapter.url) .appendEncodedPath(chapter.url)
@ -205,11 +217,13 @@ class MangaMutiny : HttpSource() {
override fun searchMangaParse(response: Response): MangasPage = mangaParse(response) override fun searchMangaParse(response: Response): MangasPage = mangaParse(response)
// commonly functions // commonly used functions
private fun mangaParse(response: Response): MangasPage { private fun mangaParse(response: Response): MangasPage {
val mangasPage = ArrayList<SManga>() val mangasPage = ArrayList<SManga>()
val responseBody = response.body() val responseBody = response.body()
var totalObjects = 0
if (responseBody != null) { if (responseBody != null) {
val rootNode = parser.parse(responseBody.charStream()) val rootNode = parser.parse(responseBody.charStream())
@ -227,12 +241,24 @@ class MangaMutiny : HttpSource() {
} }
) )
} }
// total number of manga the server found in its database
// and is returning paginated page by page:
totalObjects = rootObject.getNullable("total")?.asInt ?: 0
} }
responseBody.close() responseBody.close()
} }
return MangasPage(mangasPage, mangasPage.size == fetchAmount) val skipped = response.request().url().queryParameter("skip")?.toInt() ?: 0
val moreElementsToSkip = skipped + fetchAmount < totalObjects
val pageSizeEqualsFetchAmount = mangasPage.size == fetchAmount
val hasMorePages = pageSizeEqualsFetchAmount && moreElementsToSkip
return MangasPage(mangasPage, hasMorePages)
} }
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
@ -260,32 +286,47 @@ class MangaMutiny : HttpSource() {
} }
private fun mangaRequest(page: Int, query: String? = null, filters: FilterList? = null): Request { private fun mangaRequest(page: Int, query: String? = null, filters: FilterList? = null): Request {
val uri = Uri.parse(baseUrl).buildUpon() val forWebView = isForWebView()
uri.appendEncodedPath(apiMangaUrlPath)
val uri = if (forWebView) {
Uri.parse(baseUrl).buildUpon().apply {
appendEncodedPath(webViewMultipleMangaPath)
}
} else {
Uri.parse(baseUrlAPI).buildUpon().apply {
appendEncodedPath(apiMangaUrlPath)
}
}
if (query?.isNotBlank() == true) { if (query?.isNotBlank() == true) {
uri.appendQueryParameter("text", query) uri.appendQueryParameter("text", query)
} }
if (filters != null) { val applicableFilters = if (filters != null && filters.isNotEmpty()) {
val uriParameterMap = mutableMapOf<String, String>() filters
for (singleFilter in filters) {
if (singleFilter is UriFilter) {
singleFilter.potentiallyAddToUriParameterMap(uriParameterMap)
}
}
for (uriParameter in uriParameterMap) {
uri.appendQueryParameter(uriParameter.key, uriParameter.value)
}
} else { } else {
uri.appendQueryParameter("sort", "-rating -ratingCount") FilterList(SortFilter())
} }
uri.appendQueryParameter("limit", fetchAmount.toString())
if (page != 1) { val uriParameterMap = mutableMapOf<String, String>()
uri.appendQueryParameter("skip", (page * fetchAmount).toString())
for (singleFilter in applicableFilters) {
if (singleFilter is UriFilter) {
singleFilter.addParameter(uriParameterMap)
}
} }
for (uriParameter in uriParameterMap) {
uri.appendQueryParameter(uriParameter.key, uriParameter.value)
}
if (!forWebView) {
uri.appendQueryParameter("limit", fetchAmount.toString())
if (page != 1) {
uri.appendQueryParameter("skip", ((page - 1) * fetchAmount).toString())
}
}
return GET(uri.build().toString(), headers) return GET(uri.build().toString(), headers)
} }
@ -307,39 +348,40 @@ class MangaMutiny : HttpSource() {
} }
private interface UriFilter { private interface UriFilter {
fun potentiallyAddToUriParameterMap(parameterMap: MutableMap<String, String>) val uriParam: () -> String
fun appendValueToKeyInUriParameterMap(parameterMap: MutableMap<String, String>, parameterName: String, additionalValue: String) { val shouldAdd: () -> Boolean
if (additionalValue.isNotEmpty()) { val getParameter: () -> String
val newParameterValueBuilder = StringBuilder()
if (parameterMap[parameterName] != null) {
newParameterValueBuilder.append(parameterMap[parameterName] + " ")
}
newParameterValueBuilder.append(additionalValue)
parameterMap[parameterName] = newParameterValueBuilder.toString() fun addParameter(parameterMap: MutableMap<String, String>) {
if (shouldAdd()) {
val newParameterValueBuilder = StringBuilder()
if (parameterMap[uriParam()] != null) {
newParameterValueBuilder.append(parameterMap[uriParam()] + " ")
}
newParameterValueBuilder.append(getParameter())
parameterMap[uriParam()] = newParameterValueBuilder.toString()
} }
} }
} }
private open class UriSelectFilter(
private abstract class UriSelectFilter(
displayName: String, displayName: String,
val uriParam: String, override val uriParam: () -> String,
val vals: Array<Pair<String, String>>, val vals: Array<Pair<String, String>>,
val firstIsUnspecified: Boolean = true, val defaultValue: Int = 0
defaultValue: Int = 0
) : ) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter { Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
// If not otherwise specified, any new parameter will overwrite any existing parameter in the parameter map override val shouldAdd = fun() =
override fun potentiallyAddToUriParameterMap(parameterMap: MutableMap<String, String>) { this.state != defaultValue
if (state != 0 || !firstIsUnspecified) {
parameterMap[uriParam] = vals[state].first override val getParameter = fun() = vals[state].first
}
}
} }
private class StatusFilter : UriSelectFilter( private class StatusFilter : UriSelectFilter(
"Status", "Status",
"status", fun() = "status",
arrayOf( arrayOf(
Pair("", "All"), Pair("", "All"),
Pair("completed", "Completed"), Pair("completed", "Completed"),
@ -349,7 +391,7 @@ class MangaMutiny : HttpSource() {
private class CategoryFilter : UriSelectFilter( private class CategoryFilter : UriSelectFilter(
"Category", "Category",
"tags", fun() = "tags",
arrayOf( arrayOf(
Pair("", "All"), Pair("", "All"),
Pair("josei", "Josei"), Pair("josei", "Josei"),
@ -357,141 +399,159 @@ class MangaMutiny : HttpSource() {
Pair("shoujo", "Shoujo"), Pair("shoujo", "Shoujo"),
Pair("shounen", "Shounen") Pair("shounen", "Shounen")
) )
) { )
override fun potentiallyAddToUriParameterMap(parameterMap: MutableMap<String, String>) =
appendValueToKeyInUriParameterMap(parameterMap, uriParam, vals[state].first)
}
// A single filter: either a genre or a format filter // A single filter: either a genre or a format filter
private class GenreFilter(val uriParam: String, displayName: String) : Filter.CheckBox(displayName) private class GenreOrFormatFilter(val uriParam: String, displayName: String) :
Filter.CheckBox(displayName)
// A collection of genre or format filters // A collection of genre or format filters
private abstract class GenreFilterList(name: String, elementList: List<GenreFilter>) : Filter.Group<GenreFilter>(name, elementList), UriFilter { private abstract class GenreOrFormatFilterList(name: String, specificUriParam: String, elementList: List<GenreOrFormatFilter>) : Filter.Group<GenreOrFormatFilter>(name, elementList), UriFilter {
override fun potentiallyAddToUriParameterMap(parameterMap: MutableMap<String, String>) {
val genresParameterValue = state.filter { it.state }.joinToString(" ") { it.uriParam } override val shouldAdd = fun() = state.any { it.state }
if (genresParameterValue.isNotEmpty()) {
appendValueToKeyInUriParameterMap(parameterMap, "tags", genresParameterValue) override val getParameter = fun() =
} state.filter { it.state }.joinToString(" ") { it.uriParam }
}
override val uriParam = fun() = if (isForWebView()) specificUriParam else "tags"
} }
// Actual genere filter list
private class GenresFilter : GenreFilterList( // Generes filter list
private class GenresFilter : GenreOrFormatFilterList(
"Genres", "Genres",
"genres",
listOf( listOf(
GenreFilter("action", "action"), GenreOrFormatFilter("action", "action"),
GenreFilter("adult", "adult"), GenreOrFormatFilter("adult", "adult"),
GenreFilter("adventure", "adventure"), GenreOrFormatFilter("adventure", "adventure"),
GenreFilter("aliens", "aliens"), GenreOrFormatFilter("aliens", "aliens"),
GenreFilter("animals", "animals"), GenreOrFormatFilter("animals", "animals"),
GenreFilter("comedy", "comedy"), GenreOrFormatFilter("comedy", "comedy"),
GenreFilter("cooking", "cooking"), GenreOrFormatFilter("cooking", "cooking"),
GenreFilter("crossdressing", "crossdressing"), GenreOrFormatFilter("crossdressing", "crossdressing"),
GenreFilter("delinquents", "delinquents"), GenreOrFormatFilter("delinquents", "delinquents"),
GenreFilter("demons", "demons"), GenreOrFormatFilter("demons", "demons"),
GenreFilter("drama", "drama"), GenreOrFormatFilter("drama", "drama"),
GenreFilter("ecchi", "ecchi"), GenreOrFormatFilter("ecchi", "ecchi"),
GenreFilter("fantasy", "fantasy"), GenreOrFormatFilter("fantasy", "fantasy"),
GenreFilter("gender_bender", "gender bender"), GenreOrFormatFilter("gender_bender", "gender bender"),
GenreFilter("genderswap", "genderswap"), GenreOrFormatFilter("genderswap", "genderswap"),
GenreFilter("ghosts", "ghosts"), GenreOrFormatFilter("ghosts", "ghosts"),
GenreFilter("gore", "gore"), GenreOrFormatFilter("gore", "gore"),
GenreFilter("gyaru", "gyaru"), GenreOrFormatFilter("gyaru", "gyaru"),
GenreFilter("harem", "harem"), GenreOrFormatFilter("harem", "harem"),
GenreFilter("historical", "historical"), GenreOrFormatFilter("historical", "historical"),
GenreFilter("horror", "horror"), GenreOrFormatFilter("horror", "horror"),
GenreFilter("incest", "incest"), GenreOrFormatFilter("incest", "incest"),
GenreFilter("isekai", "isekai"), GenreOrFormatFilter("isekai", "isekai"),
GenreFilter("loli", "loli"), GenreOrFormatFilter("loli", "loli"),
GenreFilter("magic", "magic"), GenreOrFormatFilter("magic", "magic"),
GenreFilter("magical_girls", "magical girls"), GenreOrFormatFilter("magical_girls", "magical girls"),
GenreFilter("mangamutiny", "mangamutiny"), GenreOrFormatFilter("mangamutiny", "mangamutiny"),
GenreFilter("martial_arts", "martial arts"), GenreOrFormatFilter("martial_arts", "martial arts"),
GenreFilter("mature", "mature"), GenreOrFormatFilter("mature", "mature"),
GenreFilter("mecha", "mecha"), GenreOrFormatFilter("mecha", "mecha"),
GenreFilter("medical", "medical"), GenreOrFormatFilter("medical", "medical"),
GenreFilter("military", "military"), GenreOrFormatFilter("military", "military"),
GenreFilter("monster_girls", "monster girls"), GenreOrFormatFilter("monster_girls", "monster girls"),
GenreFilter("monsters", "monsters"), GenreOrFormatFilter("monsters", "monsters"),
GenreFilter("mystery", "mystery"), GenreOrFormatFilter("mystery", "mystery"),
GenreFilter("ninja", "ninja"), GenreOrFormatFilter("ninja", "ninja"),
GenreFilter("office_workers", "office workers"), GenreOrFormatFilter("office_workers", "office workers"),
GenreFilter("philosophical", "philosophical"), GenreOrFormatFilter("philosophical", "philosophical"),
GenreFilter("psychological", "psychological"), GenreOrFormatFilter("psychological", "psychological"),
GenreFilter("reincarnation", "reincarnation"), GenreOrFormatFilter("reincarnation", "reincarnation"),
GenreFilter("reverse_harem", "reverse harem"), GenreOrFormatFilter("reverse_harem", "reverse harem"),
GenreFilter("romance", "romance"), GenreOrFormatFilter("romance", "romance"),
GenreFilter("school_life", "school life"), GenreOrFormatFilter("school_life", "school life"),
GenreFilter("sci_fi", "sci fi"), GenreOrFormatFilter("sci_fi", "sci fi"),
GenreFilter("sci-fi", "sci-fi"), GenreOrFormatFilter("sci-fi", "sci-fi"),
GenreFilter("sexual_violence", "sexual violence"), GenreOrFormatFilter("sexual_violence", "sexual violence"),
GenreFilter("shota", "shota"), GenreOrFormatFilter("shota", "shota"),
GenreFilter("shoujo_ai", "shoujo ai"), GenreOrFormatFilter("shoujo_ai", "shoujo ai"),
GenreFilter("shounen_ai", "shounen ai"), GenreOrFormatFilter("shounen_ai", "shounen ai"),
GenreFilter("slice_of_life", "slice of life"), GenreOrFormatFilter("slice_of_life", "slice of life"),
GenreFilter("smut", "smut"), GenreOrFormatFilter("smut", "smut"),
GenreFilter("sports", "sports"), GenreOrFormatFilter("sports", "sports"),
GenreFilter("superhero", "superhero"), GenreOrFormatFilter("superhero", "superhero"),
GenreFilter("supernatural", "supernatural"), GenreOrFormatFilter("supernatural", "supernatural"),
GenreFilter("survival", "survival"), GenreOrFormatFilter("survival", "survival"),
GenreFilter("time_travel", "time travel"), GenreOrFormatFilter("time_travel", "time travel"),
GenreFilter("tragedy", "tragedy"), GenreOrFormatFilter("tragedy", "tragedy"),
GenreFilter("video_games", "video games"), GenreOrFormatFilter("video_games", "video games"),
GenreFilter("virtual_reality", "virtual reality"), GenreOrFormatFilter("virtual_reality", "virtual reality"),
GenreFilter("webtoons", "webtoons"), GenreOrFormatFilter("webtoons", "webtoons"),
GenreFilter("wuxia", "wuxia"), GenreOrFormatFilter("wuxia", "wuxia"),
GenreFilter("zombies", "zombies") GenreOrFormatFilter("zombies", "zombies")
) )
) )
// Actual format filter List // Actual format filter List
private class FormatsFilter : GenreFilterList( private class FormatsFilter : GenreOrFormatFilterList(
"Formats", "Formats",
"formats",
listOf( listOf(
GenreFilter("4-koma", "4-koma"), GenreOrFormatFilter("4-koma", "4-koma"),
GenreFilter("adaptation", "adaptation"), GenreOrFormatFilter("adaptation", "adaptation"),
GenreFilter("anthology", "anthology"), GenreOrFormatFilter("anthology", "anthology"),
GenreFilter("award_winning", "award winning"), GenreOrFormatFilter("award_winning", "award winning"),
GenreFilter("doujinshi", "doujinshi"), GenreOrFormatFilter("doujinshi", "doujinshi"),
GenreFilter("fan_colored", "fan colored"), GenreOrFormatFilter("fan_colored", "fan colored"),
GenreFilter("full_color", "full color"), GenreOrFormatFilter("full_color", "full color"),
GenreFilter("long_strip", "long strip"), GenreOrFormatFilter("long_strip", "long strip"),
GenreFilter("official_colored", "official colored"), GenreOrFormatFilter("official_colored", "official colored"),
GenreFilter("oneshot", "oneshot"), GenreOrFormatFilter("oneshot", "oneshot"),
GenreFilter("web_comic", "web comic") GenreOrFormatFilter("web_comic", "web comic")
) ),
) )
private class SortFilter : UriSelectFilter( private class SortFilter : UriSelectFilter(
"Sort", "Sort",
"sort", fun() = "sort",
arrayOf( arrayOf(
Pair("-rating -ratingCount", "Popular"), Pair("title", "Name"),
Pair("-lastReleasedAt", "Last update"), Pair("-lastReleasedAt", "Last update"),
Pair("-createdAt", "Newest"), Pair("-createdAt", "Newest"),
Pair("title", "Name") Pair("-rating -ratingCount", "Popular")
), ),
firstIsUnspecified = false, defaultValue = 3
defaultValue = 0 ) {
) override val shouldAdd = fun() = if (isForWebView()) state != defaultValue else true
private class AuthorFilter : Filter.Text("Manga Author & Artist"), UriFilter { override val getParameter = fun(): String {
override fun potentiallyAddToUriParameterMap(parameterMap: MutableMap<String, String>) { return if (isForWebView()) {
if (state.isNotEmpty()) { this.state.toString()
parameterMap["creator"] = state } else {
this.vals[this.state].first
} }
} }
} }
/**The scanlator filter exists on the mangamutiny website website, however it doesn't work. private class AuthorFilter() : Filter.Text("Manga Author & Artist"), UriFilter {
override val uriParam = fun() = "creator"
override val shouldAdd = fun() = state.isNotEmpty()
override val getParameter = fun(): String = state
}
/**The scanlator filter exists on the mangamutiny website, however it doesn't work.
This should stay disabled in the extension until it's properly implemented on the website, This should stay disabled in the extension until it's properly implemented on the website,
otherwise users may be confused by searches that return no results.**/ otherwise users may be confused by searches that return no results.**/
/* /*
private class ScanlatorFilter : Filter.Text("Scanlator Name"), UriFilter { private class ScanlatorFilter : Filter.Text("Scanlator Name"), UriFilter {
override fun potentiallyAddToUriParameterMap(parameterMap: MutableMap<String, String>) { override val uriParam = fun() = "scanlator"
if (state.isNotEmpty()) {
parameterMap["scanlator"] = state override val shouldAdd = fun() = state.isNotEmpty()
}
} override val getParameter = fun(): String = state
} }
*/ */
} }
private fun isForWebView(): Boolean =
Thread.currentThread().stackTrace.map { it.methodName }
.firstOrNull {
it.contains("WebView", true) && !it.contains("isForWebView")
} != null