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'
pkgNameSuffix = "en.mangamutiny"
extClass = '.MangaMutiny'
extVersionCode = 4
extVersionCode = 5
libVersion = '1.2'
containsNsfw = true
}

View File

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