Remove Manga-Flix and Manga Mutiny (#13581)
* Remove Manga-Flix and Manga Mutiny * use URL for autocloser regex * fix yaml escape Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2
.github/workflows/issue_moderator.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": ".*(team\\s*x|tqneplus|manga\\s*disk|komiktap|gourmet\\s*scans|manga\\s*crimson|mangawow|voidscans|hikari\\s*scans|mangagegecesi|piedpiperfb|knightnoscanlations|ahstudios|mangagecesi|nartag|xxx\\s*yaoi|yaoi\\s*fan\\s*clube|luminous|dragontea|manhwaid\\.org|hunters\\s*scan|mnhaestate|swat\\s*manga|manga\\s*swat|reset(?:\\s*|-)scan|aresmanga).*",
|
||||
"regex": ".*(team\\s*x|tqneplus|manga\\s*disk|komiktap|gourmet\\s*scans|manga\\s*crimson|mangawow|voidscans|hikari\\s*scans|mangagegecesi|piedpiperfb|knightnoscanlations|ahstudios|mangagecesi|nartag|xxx\\s*yaoi|yaoi\\s*fan\\s*clube|luminous|dragontea|manhwaid\\.org|hunters\\s*scan|mnhaestate|swat\\s*manga|manga\\s*swat|reset(?:\\s*|-)scan|aresmanga|manga-flix\\.com).*",
|
||||
"ignoreCase": true,
|
||||
"message": "{match} will not be added back as the Scanlator team has requested it to be removed. Read #3475 for more information"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 83 KiB |
@ -1,31 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.mangaflix
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class MangaFlix : Madara(
|
||||
"مانجا فليكس",
|
||||
"https://www.manga-flix.com",
|
||||
"ar",
|
||||
dateFormat = SimpleDateFormat("dd/MM/yyy", Locale.ROOT)
|
||||
) {
|
||||
override val useNewChapterEndpoint = true
|
||||
|
||||
override val mangaDetailsSelectorStatus = "div.summarys"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = super.chapterFromElement(element)
|
||||
|
||||
with(element) {
|
||||
selectFirst(chapterUrlSelector)?.let { urlElement ->
|
||||
// Using .text() on the <a> tag is too broad
|
||||
chapter.name = urlElement.selectFirst("> p")!!.text()
|
||||
}
|
||||
}
|
||||
|
||||
return chapter
|
||||
}
|
||||
}
|
@ -493,7 +493,6 @@ class MadaraGenerator : ThemeSourceGenerator {
|
||||
SingleLang("مانجا العاشق", "https://3asq.org", "ar", className = "Manga3asq", overrideVersionCode = 2),
|
||||
SingleLang("مانجا العرب Manga Alarab", "https://manga-alarab.com", "ar", className = "MangAlarab", overrideVersionCode = 1),
|
||||
SingleLang("مانجا عرب تيم Manga Arab Team", "https://mangaarbteam.com", "ar", className = "MangaArabTeam", overrideVersionCode = 1),
|
||||
SingleLang("مانجا فليكس", "https://www.manga-flix.com", "ar", className = "MangaFlix", overrideVersionCode = 1),
|
||||
SingleLang("مانجا ليك", "https://mangalek.com", "ar", className = "Mangalek", overrideVersionCode = 1),
|
||||
SingleLang("مانجا لينك", "https://mangalink.io", "ar", className = "MangaLinkio", overrideVersionCode = 2),
|
||||
)
|
||||
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi.extension">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".en.mangamutiny.MangaMutinyUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="mangamutiny.org"
|
||||
android:pathPattern="/title/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,13 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Manga Mutiny'
|
||||
pkgNameSuffix = "en.mangamutiny"
|
||||
extClass = '.MangaMutiny'
|
||||
extVersionCode = 9
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 23 KiB |
@ -1,441 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangamutiny
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaMutiny : HttpSource() {
|
||||
|
||||
override val name = "Manga Mutiny"
|
||||
override val baseUrl = "https://mangamutiny.org"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
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")
|
||||
add("Origin", "https://mangamutiny.org")
|
||||
}
|
||||
}
|
||||
|
||||
private val apiMangaUrlPath = "v1/public/manga"
|
||||
private val apiChapterUrlPath = "v1/public/chapter"
|
||||
|
||||
private val fetchAmount = 21
|
||||
|
||||
companion object {
|
||||
const val PREFIX_ID_SEARCH = "slug:"
|
||||
}
|
||||
|
||||
// Popular manga
|
||||
override fun popularMangaRequest(page: Int): Request = mangaRequest(page)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = mangaParse(response)
|
||||
|
||||
// Chapters
|
||||
override fun chapterListRequest(manga: SManga): Request =
|
||||
mangaDetailsRequestCommon(manga, false)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val responseBody = response.body
|
||||
|
||||
return responseBody?.use {
|
||||
json.decodeFromString(ListChapterDS, it.string()).also {
|
||||
responseBody.close()
|
||||
}
|
||||
} ?: listOf()
|
||||
}
|
||||
|
||||
// latest
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
mangaRequest(page, filters = FilterList(SortFilter().apply { this.state = 1 }))
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = mangaParse(response)
|
||||
|
||||
// browse + latest + search
|
||||
override fun mangaDetailsRequest(manga: SManga): Request = mangaDetailsRequestCommon(manga)
|
||||
|
||||
private fun mangaDetailsRequestCommon(manga: SManga, lite: Boolean = true): Request {
|
||||
val uri = if (isForWebView()) {
|
||||
Uri.parse(baseUrl).buildUpon()
|
||||
.appendEncodedPath(webViewSingleMangaPath)
|
||||
.appendPath(manga.url)
|
||||
} 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)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val responseBody = response.body
|
||||
|
||||
if (responseBody != null) {
|
||||
return responseBody.use {
|
||||
json.decodeFromString(SMangaDS, it.string())
|
||||
}
|
||||
} else {
|
||||
throw IllegalStateException("Response code ${response.code}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val uri = Uri.parse(baseUrlAPI).buildUpon()
|
||||
.appendEncodedPath(apiChapterUrlPath)
|
||||
.appendEncodedPath(chapter.url)
|
||||
|
||||
return GET(uri.build().toString(), headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val responseBody = response.body
|
||||
|
||||
return responseBody?.use {
|
||||
json.decodeFromString(ListPageDS, it.string())
|
||||
} ?: listOf()
|
||||
}
|
||||
|
||||
// Search
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||
mangaRequest(page, query, filters)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = mangaParse(response)
|
||||
|
||||
// commonly used functions
|
||||
private fun mangaParse(response: Response): MangasPage {
|
||||
val responseBody = response.body
|
||||
|
||||
return if (responseBody != null) {
|
||||
val deserializationResult = json.decodeFromString(PageInfoDS, responseBody.string())
|
||||
val totalObjects = deserializationResult.second
|
||||
val skipped = response.request.url.queryParameter("skip")?.toInt() ?: 0
|
||||
val moreElementsToSkip = skipped + fetchAmount < totalObjects
|
||||
val pageSizeEqualsFetchAmount = deserializationResult.first.size == fetchAmount
|
||||
val hasMorePages = pageSizeEqualsFetchAmount && moreElementsToSkip
|
||||
|
||||
MangasPage(deserializationResult.first, hasMorePages)
|
||||
} else {
|
||||
MangasPage(listOf(), false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
|
||||
return if (query.startsWith(PREFIX_ID_SEARCH)) {
|
||||
val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
|
||||
|
||||
val tempManga = SManga.create().apply {
|
||||
url = realQuery
|
||||
}
|
||||
|
||||
client.newCall(mangaDetailsRequestCommon(tempManga, true))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val details = mangaDetailsParse(response)
|
||||
MangasPage(listOf(details), false)
|
||||
}
|
||||
} else {
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mangaRequest(page: Int, query: String? = null, filters: FilterList? = null): Request {
|
||||
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)
|
||||
}
|
||||
|
||||
val applicableFilters = if (filters != null && filters.isNotEmpty()) {
|
||||
filters
|
||||
} else {
|
||||
FilterList(SortFilter())
|
||||
}
|
||||
|
||||
val uriParameterMap = mutableMapOf<String, String>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Filter
|
||||
override fun getFilterList(): FilterList {
|
||||
return FilterList(
|
||||
StatusFilter(),
|
||||
CategoryFilter(),
|
||||
GenresFilter(),
|
||||
FormatsFilter(),
|
||||
SortFilter(),
|
||||
AuthorFilter()
|
||||
// ScanlatorFilter()
|
||||
)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
private interface UriFilter {
|
||||
val uriParam: () -> String
|
||||
val shouldAdd: () -> Boolean
|
||||
val getParameter: () -> String
|
||||
|
||||
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 abstract class UriSelectFilter(
|
||||
displayName: String,
|
||||
override val uriParam: () -> String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
val defaultValue: Int = 0
|
||||
) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||
|
||||
override val shouldAdd = fun() =
|
||||
this.state != defaultValue
|
||||
|
||||
override val getParameter = fun() = vals[state].first
|
||||
}
|
||||
|
||||
private class StatusFilter : UriSelectFilter(
|
||||
"Status",
|
||||
fun() = "status",
|
||||
arrayOf(
|
||||
Pair("", "All"),
|
||||
Pair("completed", "Completed"),
|
||||
Pair("ongoing", "Ongoing")
|
||||
)
|
||||
)
|
||||
|
||||
private class CategoryFilter : UriSelectFilter(
|
||||
"Category",
|
||||
fun() = "tags",
|
||||
arrayOf(
|
||||
Pair("", "All"),
|
||||
Pair("josei", "Josei"),
|
||||
Pair("seinen", "Seinen"),
|
||||
Pair("shoujo", "Shoujo"),
|
||||
Pair("shounen", "Shounen")
|
||||
)
|
||||
)
|
||||
|
||||
// A single filter: either a genre or a format filter
|
||||
private class GenreOrFormatFilter(val uriParam: String, displayName: String) :
|
||||
Filter.CheckBox(displayName)
|
||||
|
||||
// A collection of genre or format filters
|
||||
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"
|
||||
}
|
||||
|
||||
// Generes filter list
|
||||
private class GenresFilter : GenreOrFormatFilterList(
|
||||
"Genres",
|
||||
"genres",
|
||||
listOf(
|
||||
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 : GenreOrFormatFilterList(
|
||||
"Formats",
|
||||
"formats",
|
||||
listOf(
|
||||
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",
|
||||
fun() = "sort",
|
||||
arrayOf(
|
||||
Pair("title", "Name"),
|
||||
Pair("-lastReleasedAt", "Last update"),
|
||||
Pair("-createdAt", "Newest"),
|
||||
Pair("-rating -ratingCount", "Popular")
|
||||
),
|
||||
defaultValue = 3
|
||||
) {
|
||||
override val shouldAdd = fun() = if (isForWebView()) state != defaultValue else true
|
||||
|
||||
override val getParameter = fun(): String {
|
||||
return if (isForWebView()) {
|
||||
this.state.toString()
|
||||
} else {
|
||||
this.vals[this.state].first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
@ -1,183 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangamutiny
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
private fun JsonElement?.primitiveContent(): String? {
|
||||
if (this is JsonNull) return null
|
||||
return this?.jsonPrimitive?.content
|
||||
}
|
||||
private fun JsonElement?.primitiveInt(): Int? {
|
||||
if (this is JsonNull) return null
|
||||
return this?.jsonPrimitive?.int
|
||||
}
|
||||
private fun JsonElement?.primitiveFloat(): Float? {
|
||||
if (this is JsonNull) return null
|
||||
return this?.jsonPrimitive?.float
|
||||
}
|
||||
|
||||
private val jsonObjectToMapSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
|
||||
|
||||
object PageInfoDS : DeserializationStrategy<Pair<List<SManga>, Int>> {
|
||||
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): Pair<List<SManga>, Int> {
|
||||
require(decoder is JsonDecoder)
|
||||
val json = decoder.json
|
||||
val jsonElement = decoder.decodeJsonElement()
|
||||
require(jsonElement is JsonObject)
|
||||
val items = (jsonElement["items"] as JsonArray).map { json.decodeFromJsonElement(SMangaDS, it) }
|
||||
val total = jsonElement["total"]?.jsonPrimitive?.int
|
||||
|
||||
require(total != null)
|
||||
return Pair(items, total)
|
||||
}
|
||||
}
|
||||
|
||||
object SMangaDS : DeserializationStrategy<SManga> {
|
||||
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): SManga {
|
||||
require(decoder is JsonDecoder)
|
||||
val jsonElement = decoder.decodeJsonElement()
|
||||
require(jsonElement is JsonObject)
|
||||
val title = jsonElement["title"].primitiveContent()
|
||||
val slug = jsonElement["slug"].primitiveContent()
|
||||
val thumbnail = jsonElement["thumbnail"].primitiveContent()
|
||||
|
||||
val status: Int = jsonElement["status"].primitiveContent()?.let {
|
||||
when (it) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
} ?: SManga.UNKNOWN
|
||||
|
||||
val summary: String? = jsonElement["summary"].primitiveContent()
|
||||
val artists: String? = jsonElement["artists"].primitiveContent()
|
||||
val authors: String? = jsonElement["authors"].primitiveContent()
|
||||
val tags: String? =
|
||||
jsonElement["tags"]?.jsonArray?.mapNotNull { it.primitiveContent() }?.joinToString()
|
||||
|
||||
require(title != null && slug != null)
|
||||
return SManga.create().apply {
|
||||
this.title = title
|
||||
this.url = slug
|
||||
this.thumbnail_url = thumbnail
|
||||
|
||||
this.status = status
|
||||
this.description = summary
|
||||
this.artist = artists
|
||||
this.author = authors
|
||||
this.genre = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ListChapterDS : DeserializationStrategy<List<SChapter>> {
|
||||
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): List<SChapter> {
|
||||
require(decoder is JsonDecoder)
|
||||
val json = decoder.json
|
||||
val jsonElement = decoder.decodeJsonElement()
|
||||
require(jsonElement is JsonObject)
|
||||
|
||||
val jsonElementChapters = jsonElement["chapters"]?.jsonArray
|
||||
require(jsonElementChapters != null)
|
||||
|
||||
return jsonElementChapters.map { chapter ->
|
||||
json.decodeFromJsonElement(SChapterDS, chapter)
|
||||
}.apply {
|
||||
if (this.size == 1) this.first().chapter_number = 1F
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object SChapterDS : DeserializationStrategy<SChapter> {
|
||||
|
||||
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): SChapter {
|
||||
require(decoder is JsonDecoder)
|
||||
val jsonElement = decoder.decodeJsonElement()
|
||||
require(jsonElement is JsonObject)
|
||||
val volume: Int? = jsonElement["volume"].primitiveInt()
|
||||
val chapter: Float? = jsonElement["chapter"].primitiveFloat()
|
||||
val title: String? = jsonElement["title"].primitiveContent()
|
||||
val slug: String? = jsonElement["slug"].primitiveContent()
|
||||
val releasedAt: String? = jsonElement["releasedAt"].primitiveContent()
|
||||
|
||||
require(slug != null && releasedAt != null)
|
||||
return SChapter.create().apply {
|
||||
if (chapter != null) this.chapter_number = chapter
|
||||
this.name = chapterTitleBuilder(volume, title, chapter)
|
||||
this.url = slug
|
||||
this.date_upload = dateFormatter.parse(releasedAt)?.time ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
/**
|
||||
* Converts this Float into a String, removing any trailing .0
|
||||
*/
|
||||
private fun Float.toStringWithoutDotZero(): String = when (this % 1) {
|
||||
0F -> this.toInt().toString()
|
||||
else -> this.toString()
|
||||
}
|
||||
|
||||
private fun chapterTitleBuilder(volume: Int?, title: String?, chapter: Float?): String {
|
||||
val chapterTitle = StringBuilder()
|
||||
if (volume != null) {
|
||||
chapterTitle.append("Vol. $volume ")
|
||||
}
|
||||
if (chapter != null) {
|
||||
chapterTitle.append("Chapter ${chapter.toStringWithoutDotZero()}")
|
||||
}
|
||||
if (title != null && title != "") {
|
||||
if (chapterTitle.isNotEmpty()) chapterTitle.append(": ")
|
||||
chapterTitle.append(title)
|
||||
}
|
||||
return chapterTitle.toString()
|
||||
}
|
||||
}
|
||||
|
||||
object ListPageDS : DeserializationStrategy<List<Page>> {
|
||||
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): List<Page> {
|
||||
require(decoder is JsonDecoder)
|
||||
val jsonElement = decoder.decodeJsonElement()
|
||||
require(jsonElement is JsonObject)
|
||||
|
||||
val storage: String? = jsonElement["storage"].primitiveContent()
|
||||
val manga: String? = jsonElement["manga"].primitiveContent()
|
||||
val id: String? = jsonElement["id"].primitiveContent()
|
||||
val images: List<String>? =
|
||||
jsonElement["images"]?.jsonArray?.mapNotNull { it.primitiveContent() }
|
||||
|
||||
require(storage != null && manga != null && id != null && images != null)
|
||||
val chapterUrl = "$storage/$manga/$id/"
|
||||
return images.mapIndexed { index, pageSuffix -> Page(index, "", chapterUrl + pageSuffix) }
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangamutiny
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://mangamutiny.org/title/xxx intents and redirects them to
|
||||
* the main tachiyomi process. The idea is to not install the intent filter unless
|
||||
* you have this extension installed, but still let the main tachiyomi app control
|
||||
* things.
|
||||
*/
|
||||
class MangaMutinyUrlActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val slug = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${MangaMutiny.PREFIX_ID_SEARCH}$slug")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("MangaMutinyUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("MangaMutinyUrlActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|