MangaPark v5 (#19341)
* MangaPark: cleanup fresh start * basic functionality * webview urls * filters * review changes * description logic & id in url & a filter * bump versionId to differentiate from old v2 extension which was removed * update icons * Domain preference
|
@ -1,22 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.mangapark.MangaParkUrlActivity"
|
||||
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="mangapark.net"
|
||||
android:pathPattern="/comic/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
<manifest />
|
||||
|
|
|
@ -3,15 +3,11 @@ apply plugin: 'kotlin-android'
|
|||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'MangaPark v3'
|
||||
extName = 'MangaPark'
|
||||
pkgNameSuffix = 'all.mangapark'
|
||||
extClass = '.MangaParkFactory'
|
||||
extVersionCode = 18
|
||||
extVersionCode = 19
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib-cryptoaes'))
|
||||
}
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 40 KiB |
|
@ -0,0 +1,46 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||
|
||||
import android.util.Log
|
||||
import android.webkit.CookieManager
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class CookieInterceptor(
|
||||
private val domain: String,
|
||||
private val key: String,
|
||||
private val value: String,
|
||||
) : Interceptor {
|
||||
|
||||
init {
|
||||
val url = "https://$domain/"
|
||||
val cookie = "$key=$value; Domain=$domain; Path=/"
|
||||
setCookie(url, cookie)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (!request.url.host.endsWith(domain)) return chain.proceed(request)
|
||||
|
||||
val cookie = "$key=$value"
|
||||
val cookieList = request.header("Cookie")?.split("; ") ?: emptyList()
|
||||
if (cookie in cookieList) return chain.proceed(request)
|
||||
|
||||
setCookie("https://$domain/", "$cookie; Domain=$domain; Path=/")
|
||||
val prefix = "$key="
|
||||
val newCookie = buildList(cookieList.size + 1) {
|
||||
cookieList.filterNotTo(this) { it.startsWith(prefix) }
|
||||
add(cookie)
|
||||
}.joinToString("; ")
|
||||
val newRequest = request.newBuilder().header("Cookie", newCookie).build()
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
private fun setCookie(url: String, value: String) {
|
||||
try {
|
||||
CookieManager.getInstance().setCookie(url, value)
|
||||
} catch (e: Exception) {
|
||||
// Probably running on Tachidesk
|
||||
Log.e("MangaPark", "failed to set cookie", e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,289 +1,251 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
|
||||
import android.app.Application
|
||||
import android.widget.Toast
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
open class MangaPark(
|
||||
final override val lang: String,
|
||||
private val siteLang: String,
|
||||
) : ParsedHttpSource() {
|
||||
class MangaPark(
|
||||
override val lang: String,
|
||||
private val siteLang: String = lang,
|
||||
) : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val name: String = "MangaPark v3"
|
||||
|
||||
override val baseUrl: String = "https://mangapark.net"
|
||||
override val name = "MangaPark"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val id: Long = when (lang) {
|
||||
"zh-Hans" -> 6306867705763005424
|
||||
"zh-Hant" -> 4563855043528673539
|
||||
else -> super.id
|
||||
}
|
||||
override val versionId = 2
|
||||
|
||||
private val preference =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
||||
private val domain =
|
||||
preference.getString(MIRROR_PREF_KEY, MIRROR_PREF_DEFAULT) ?: MIRROR_PREF_DEFAULT
|
||||
|
||||
override val baseUrl = "https://$domain"
|
||||
|
||||
private val apiUrl = "$baseUrl/apo/"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val mpFilters = MangaParkFilters()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(CookieInterceptor(domain, "nsfw", "2"))
|
||||
.rateLimitHost(apiUrl.toHttpUrl(), 1)
|
||||
.build()
|
||||
|
||||
// Site Browse Helper
|
||||
private fun browseMangaSelector(): String = "div#subject-list div.col"
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
private fun browseNextPageSelector(): String =
|
||||
"div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)"
|
||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
private fun browseMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
setUrlWithoutDomain(element.select("a.fw-bold").attr("href"))
|
||||
title = element.select("a.fw-bold").text()
|
||||
thumbnail_url = element.select("a.position-relative img").attr("abs:src")
|
||||
}
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/browse?sort=update&page=$page")
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val payload = GraphQL(
|
||||
SearchVariables(
|
||||
SearchPayload(
|
||||
page = page,
|
||||
size = size,
|
||||
query = query.takeUnless(String::isEmpty),
|
||||
incGenres = filters.firstInstanceOrNull<GenreFilter>()?.included,
|
||||
excGenres = filters.firstInstanceOrNull<GenreFilter>()?.excluded,
|
||||
incTLangs = listOf(siteLang),
|
||||
incOLangs = filters.firstInstanceOrNull<OriginalLanguageFilter>()?.checked,
|
||||
sortby = filters.firstInstanceOrNull<SortFilter>()?.selected,
|
||||
chapCount = filters.firstInstanceOrNull<ChapterCountFilter>()?.selected,
|
||||
origStatus = filters.firstInstanceOrNull<OriginalStatusFilter>()?.selected,
|
||||
siteStatus = filters.firstInstanceOrNull<UploadStatusFilter>()?.selected,
|
||||
),
|
||||
),
|
||||
SEARCH_QUERY,
|
||||
).toJsonRequestBody()
|
||||
|
||||
override fun latestUpdatesSelector(): String = browseMangaSelector()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = browseNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
browseMangaFromElement(element)
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/browse?sort=d007&page=$page")
|
||||
|
||||
override fun popularMangaSelector(): String = browseMangaSelector()
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = browseNextPageSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga =
|
||||
browseMangaFromElement(element)
|
||||
|
||||
// Search
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return when {
|
||||
query.startsWith(PREFIX_ID_SEARCH) -> fetchSearchIdManga(query)
|
||||
query.isNotBlank() -> fetchSearchManga(page, query)
|
||||
else -> fetchGenreSearchManga(page, filters)
|
||||
}
|
||||
}
|
||||
|
||||
// Search With Manga ID
|
||||
private fun fetchSearchIdManga(idWithPrefix: String): Observable<MangasPage> {
|
||||
val id = idWithPrefix.removePrefix(PREFIX_ID_SEARCH)
|
||||
return client.newCall(GET("$baseUrl/comic/$id", headers))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
MangasPage(listOf(mangaDetailsParse(response.asJsoup())), false)
|
||||
}
|
||||
}
|
||||
|
||||
// Search WIth Query
|
||||
private fun fetchSearchManga(page: Int, query: String): Observable<MangasPage> {
|
||||
return client.newCall(GET("$baseUrl/search?word=$query&page=$page", headers))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
// Search With Filter
|
||||
private fun fetchGenreSearchManga(page: Int, filters: FilterList): Observable<MangasPage> {
|
||||
val url = "$baseUrl/browse".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("page", page.toString()).let { mpFilters.addFiltersToUrl(it, filters) }
|
||||
|
||||
return client.newCall(GET(url, headers))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String = "div#search-list div.col"
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
"div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
setUrlWithoutDomain(element.select("a.fw-bold").attr("href"))
|
||||
title = element.select("a.fw-bold").text()
|
||||
thumbnail_url = element.select("a.position-relative img").attr("abs:src")
|
||||
}
|
||||
return POST(apiUrl, headers, payload)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val isBrowse = response.request.url.pathSegments[0] == "browse"
|
||||
val mangaSelector = if (isBrowse) browseMangaSelector() else searchMangaSelector()
|
||||
val nextPageSelector = if (isBrowse) browseNextPageSelector() else searchMangaNextPageSelector()
|
||||
runCatching(::getGenres)
|
||||
|
||||
val mangas = document.select(mangaSelector).map { element ->
|
||||
if (isBrowse) browseMangaFromElement(element) else searchMangaFromElement(element)
|
||||
}
|
||||
val result = response.parseAs<SearchResponse>()
|
||||
|
||||
val hasNextPage = document.select(nextPageSelector).first() != null
|
||||
val entries = result.data.searchComics.items.map { it.data.toSManga() }
|
||||
val hasNextPage = entries.size == size
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
// Manga Details
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#mainer div.container-fluid")
|
||||
private var genreCache: List<Pair<String, String>> = emptyList()
|
||||
private var genreFetchAttempt = 0
|
||||
|
||||
return SManga.create().apply {
|
||||
setUrlWithoutDomain(infoElement.select("h3.item-title a").attr("href"))
|
||||
private fun getGenres() {
|
||||
if (genreCache.isEmpty() && genreFetchAttempt < 3) {
|
||||
val elements = runCatching {
|
||||
client.newCall(GET("$baseUrl/search", headers)).execute()
|
||||
.use { it.asJsoup() }
|
||||
.select("div.flex-col:contains(Genres) div.whitespace-nowrap")
|
||||
}.getOrNull().orEmpty()
|
||||
|
||||
title = infoElement.select("h3.item-title").text()
|
||||
genreCache = elements.mapNotNull {
|
||||
val name = it.selectFirst("span.whitespace-nowrap")
|
||||
?.text()?.takeUnless(String::isEmpty)
|
||||
?: return@mapNotNull null
|
||||
|
||||
description = infoElement.select("div.limit-height-body")
|
||||
.select("h5.text-muted, div.limit-html")
|
||||
.joinToString("\n\n") { it.text().trim() } + "\n\nAlt. Titles" + infoElement
|
||||
.select("div.alias-set").text()
|
||||
.split("/").joinToString(", ") { it.trim() }
|
||||
val key = it.attr("q:key")
|
||||
.takeUnless(String::isEmpty) ?: return@mapNotNull null
|
||||
|
||||
author = infoElement.select("div.attr-item:contains(author) a")
|
||||
.joinToString { it.text().trim() }
|
||||
|
||||
status = infoElement.select("div.attr-item:contains(status) span")
|
||||
.text().parseStatus()
|
||||
|
||||
thumbnail_url = infoElement.select("div.detail-set div.attr-cover img").attr("abs:src")
|
||||
|
||||
genre = infoElement.select("div.attr-item:contains(genres) span span")
|
||||
.joinToString { it.text().trim() }
|
||||
Pair(name, key)
|
||||
}
|
||||
genreFetchAttempt++
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.parseStatus() = if (this == null) {
|
||||
SManga.UNKNOWN
|
||||
} else {
|
||||
when {
|
||||
this.lowercase(Locale.US).contains("ongoing") -> SManga.ONGOING
|
||||
this.lowercase(Locale.US).contains("hiatus") -> SManga.ONGOING
|
||||
this.lowercase(Locale.US).contains("completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters = mutableListOf<Filter<*>>(
|
||||
SortFilter(),
|
||||
OriginalLanguageFilter(),
|
||||
OriginalStatusFilter(),
|
||||
UploadStatusFilter(),
|
||||
ChapterCountFilter(),
|
||||
)
|
||||
|
||||
if (genreCache.isEmpty()) {
|
||||
filters += listOf(
|
||||
Filter.Separator(),
|
||||
Filter.Header("Press 'reset' to attempt to load genres"),
|
||||
)
|
||||
} else {
|
||||
filters.addAll(1, listOf(GenreFilter(genreCache)))
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val payload = GraphQL(
|
||||
IdVariables(manga.url.substringAfterLast("#")),
|
||||
DETAILS_QUERY,
|
||||
).toJsonRequestBody()
|
||||
|
||||
return POST(apiUrl, headers, payload)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val result = response.parseAs<DetailsResponse>()
|
||||
|
||||
return result.data.comic.data.toSManga()
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#")
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val payload = GraphQL(
|
||||
IdVariables(manga.url.substringAfterLast("#")),
|
||||
CHAPTERS_QUERY,
|
||||
).toJsonRequestBody()
|
||||
|
||||
return POST(apiUrl, headers, payload)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chapterListHtml = response.asJsoup().select("div.episode-list #chap-index")
|
||||
return chapterListHtml.flatMap { it.select(chapterListSelector()).map { chapElem -> chapterFromElement(chapElem) } }
|
||||
val result = response.parseAs<ChapterListResponse>()
|
||||
|
||||
return result.data.chapterList.map { it.data.toSChapter() }.reversed()
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String {
|
||||
return when (lang) {
|
||||
"en" -> "div.p-2:not(:has(.px-3))"
|
||||
// To handle both "/comic/1/test/c0-en" and "/comic/1/test/c0-en/" like url
|
||||
else -> "div.p-2:has(.px-3 a[href\$=\"$siteLang\"]), div.p-2:has(.px-3 a[href\$=\"$siteLang/\"])"
|
||||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val payload = GraphQL(
|
||||
IdVariables(chapter.url.substringAfterLast("#")),
|
||||
PAGES_QUERY,
|
||||
).toJsonRequestBody()
|
||||
|
||||
return POST(apiUrl, headers, payload)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = response.parseAs<PageListResponse>()
|
||||
|
||||
return result.data.chapterPages.data.imageFile.urlList.mapIndexed { idx, url ->
|
||||
Page(idx, "", url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a.ms-3")
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = MIRROR_PREF_KEY
|
||||
title = "Preferred Mirror"
|
||||
entries = mirrors
|
||||
entryValues = mirrors
|
||||
setDefaultValue(MIRROR_PREF_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
return SChapter.create().apply {
|
||||
name = urlElement.text().removePrefix("Ch").trim()
|
||||
date_upload = element.select("i.text-nowrap").text().parseChapterDate()
|
||||
setUrlWithoutDomain(urlElement.attr("href").removeSuffix("/"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.parseChapterDate(): Long {
|
||||
if (this == null) return 0L
|
||||
val value = this.split(' ')[0].toInt()
|
||||
|
||||
return when (this.split(' ')[1].removeSuffix("s")) {
|
||||
"sec" -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, value * -1)
|
||||
}.timeInMillis
|
||||
"min" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
}.timeInMillis
|
||||
"hour" -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
}.timeInMillis
|
||||
"day" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
}.timeInMillis
|
||||
"week" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
}.timeInMillis
|
||||
"month" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
}.timeInMillis
|
||||
"year" -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0L
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
Toast.makeText(screen.context, "Restart Tachiyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
if (document.selectFirst("div.wrapper-deleted") != null) {
|
||||
throw Exception("The chapter content seems to be deleted.\n\nContact the site owner if possible.")
|
||||
}
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
use { body.string() }.let(json::decodeFromString)
|
||||
|
||||
val script = document.selectFirst("script:containsData(imgHttpLis):containsData(amWord):containsData(amPass)")?.html()
|
||||
?: throw RuntimeException("Couldn't find script with image data.")
|
||||
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
|
||||
filterIsInstance<T>().firstOrNull()
|
||||
|
||||
val imgHttpLisString = script.substringAfter("const imgHttpLis =").substringBefore(";").trim()
|
||||
val imgHttpLis = json.parseToJsonElement(imgHttpLisString).jsonArray.map { it.jsonPrimitive.content }
|
||||
val amWord = script.substringAfter("const amWord =").substringBefore(";").trim()
|
||||
val amPass = script.substringAfter("const amPass =").substringBefore(";").trim()
|
||||
private inline fun <reified T : Any> T.toJsonRequestBody() =
|
||||
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(amPass)
|
||||
val imgAccListString = CryptoAES.decrypt(amWord.removeSurrounding("\""), evaluatedPass)
|
||||
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
|
||||
|
||||
return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) ->
|
||||
Page(i, imageUrl = "$imgUrl?$imgAcc")
|
||||
}
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException("Not Used")
|
||||
}
|
||||
|
||||
override fun getFilterList() = mpFilters.getFilterList()
|
||||
|
||||
// Unused Stuff
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun imageUrlParse(document: Document): String =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
companion object {
|
||||
private const val size = 24
|
||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
private const val MIRROR_PREF_KEY = "pref_mirror"
|
||||
private const val MIRROR_PREF_DEFAULT = "mangapark.net"
|
||||
private val mirrors = arrayOf(
|
||||
"mangapark.net",
|
||||
"mangapark.com",
|
||||
"mangapark.org",
|
||||
"mangapark.me",
|
||||
"mangapark.io",
|
||||
"mangapark.to",
|
||||
"comicpark.org",
|
||||
"comicpark.to",
|
||||
"readpark.org",
|
||||
"readpark.net",
|
||||
"parkmanga.com",
|
||||
"parkmanga.net",
|
||||
"parkmanga.org",
|
||||
"mpark.to",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
typealias SearchResponse = Data<SearchComics>
|
||||
typealias DetailsResponse = Data<ComicNode>
|
||||
typealias ChapterListResponse = Data<ChapterList>
|
||||
typealias PageListResponse = Data<ChapterPages>
|
||||
|
||||
@Serializable
|
||||
data class Data<T>(val data: T)
|
||||
|
||||
@Serializable
|
||||
data class Items<T>(val items: List<T>)
|
||||
|
||||
@Serializable
|
||||
data class SearchComics(
|
||||
@SerialName("get_searchComic") val searchComics: Items<Data<MangaParkComic>>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComicNode(
|
||||
@SerialName("get_comicNode") val comic: Data<MangaParkComic>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaParkComic(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val altNames: List<String>? = null,
|
||||
val authors: List<String>? = null,
|
||||
val artists: List<String>? = null,
|
||||
val genres: List<String>? = null,
|
||||
val originalStatus: String? = null,
|
||||
val uploadStatus: String? = null,
|
||||
val summary: String? = null,
|
||||
@SerialName("urlCoverOri") val cover: String? = null,
|
||||
val urlPath: String,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = "$urlPath#$id"
|
||||
title = name
|
||||
thumbnail_url = cover
|
||||
author = authors?.joinToString()
|
||||
artist = artists?.joinToString()
|
||||
description = buildString {
|
||||
val desc = summary?.let { Jsoup.parse(it).text() }
|
||||
val names = altNames?.takeUnless { it.isEmpty() }
|
||||
?.joinToString("\n") { "• ${it.trim()}" }
|
||||
|
||||
if (desc.isNullOrEmpty()) {
|
||||
if (!names.isNullOrEmpty()) {
|
||||
append("Alternative Names:\n", names)
|
||||
}
|
||||
} else {
|
||||
append(desc)
|
||||
if (!names.isNullOrEmpty()) {
|
||||
append("\n\nAlternative Names:\n", names)
|
||||
}
|
||||
}
|
||||
}
|
||||
genre = genres?.joinToString { it.replace("_", " ").toCamelCase() }
|
||||
status = when (originalStatus) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> {
|
||||
if (uploadStatus == "ongoing") {
|
||||
SManga.PUBLISHING_FINISHED
|
||||
} else {
|
||||
SManga.COMPLETED
|
||||
}
|
||||
}
|
||||
"hiatus" -> SManga.ON_HIATUS
|
||||
"cancelled" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun String.toCamelCase(): String {
|
||||
val result = StringBuilder(length)
|
||||
var capitalize = true
|
||||
for (char in this) {
|
||||
result.append(
|
||||
if (capitalize) {
|
||||
char.uppercase()
|
||||
} else {
|
||||
char.lowercase()
|
||||
},
|
||||
)
|
||||
capitalize = char.isWhitespace()
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChapterList(
|
||||
@SerialName("get_comicChapterList") val chapterList: List<Data<MangaParkChapter>>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaParkChapter(
|
||||
val id: String,
|
||||
@SerialName("dname") val displayName: String,
|
||||
val title: String? = null,
|
||||
val dateCreate: Long? = null,
|
||||
val dateModify: Long? = null,
|
||||
val urlPath: String,
|
||||
) {
|
||||
fun toSChapter() = SChapter.create().apply {
|
||||
url = "$urlPath#$id"
|
||||
name = buildString {
|
||||
append(displayName)
|
||||
title?.let { append(": ", it) }
|
||||
}
|
||||
date_upload = dateModify ?: dateCreate ?: 0L
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChapterPages(
|
||||
@SerialName("get_chapterNode") val chapterPages: Data<ImageFiles>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ImageFiles(
|
||||
val imageFile: UrlList,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UrlList(
|
||||
val urlList: List<String>,
|
||||
)
|
|
@ -1,112 +1,107 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class MangaParkFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = languages.map { MangaPark(it.lang, it.siteLang) }
|
||||
override fun createSources() = listOf(
|
||||
MangaPark("af"),
|
||||
MangaPark("sq"),
|
||||
MangaPark("am"),
|
||||
MangaPark("ar"),
|
||||
MangaPark("hy"),
|
||||
MangaPark("az"),
|
||||
MangaPark("be"),
|
||||
MangaPark("bn"),
|
||||
MangaPark("bs"),
|
||||
MangaPark("bg"),
|
||||
MangaPark("my"),
|
||||
MangaPark("km"),
|
||||
MangaPark("ca"),
|
||||
MangaPark("ceb"),
|
||||
MangaPark("zh"),
|
||||
MangaPark("zh-Hans", "zh_hk"),
|
||||
MangaPark("zh-Hant", "zh_tw"),
|
||||
MangaPark("hr"),
|
||||
MangaPark("cs"),
|
||||
MangaPark("da"),
|
||||
MangaPark("nl"),
|
||||
MangaPark("en"),
|
||||
MangaPark("eo"),
|
||||
MangaPark("et"),
|
||||
MangaPark("fo"),
|
||||
MangaPark("fil"),
|
||||
MangaPark("fi"),
|
||||
MangaPark("fr"),
|
||||
MangaPark("ka"),
|
||||
MangaPark("de"),
|
||||
MangaPark("el"),
|
||||
MangaPark("gn"),
|
||||
MangaPark("ht"),
|
||||
MangaPark("ha"),
|
||||
MangaPark("he"),
|
||||
MangaPark("hi"),
|
||||
MangaPark("hu"),
|
||||
MangaPark("is"),
|
||||
MangaPark("ig"),
|
||||
MangaPark("id"),
|
||||
MangaPark("ga"),
|
||||
MangaPark("it"),
|
||||
MangaPark("ja"),
|
||||
MangaPark("jv"),
|
||||
MangaPark("kk"),
|
||||
MangaPark("ko"),
|
||||
MangaPark("ku"),
|
||||
MangaPark("ky"),
|
||||
MangaPark("lo"),
|
||||
MangaPark("lv"),
|
||||
MangaPark("lt"),
|
||||
MangaPark("lb"),
|
||||
MangaPark("mk"),
|
||||
MangaPark("mg"),
|
||||
MangaPark("ms"),
|
||||
MangaPark("ml"),
|
||||
MangaPark("mt"),
|
||||
MangaPark("mi"),
|
||||
MangaPark("mo"),
|
||||
MangaPark("mn"),
|
||||
MangaPark("ne"),
|
||||
MangaPark("no"),
|
||||
MangaPark("ny"),
|
||||
MangaPark("ps"),
|
||||
MangaPark("fa"),
|
||||
MangaPark("pl"),
|
||||
MangaPark("pt"),
|
||||
MangaPark("pt-BR", "pt_br"),
|
||||
MangaPark("ro"),
|
||||
MangaPark("rm"),
|
||||
MangaPark("ru"),
|
||||
MangaPark("sm"),
|
||||
MangaPark("sr"),
|
||||
MangaPark("sh"),
|
||||
MangaPark("st"),
|
||||
MangaPark("sn"),
|
||||
MangaPark("sd"),
|
||||
MangaPark("si"),
|
||||
MangaPark("sk"),
|
||||
MangaPark("sl"),
|
||||
MangaPark("so"),
|
||||
MangaPark("es"),
|
||||
MangaPark("es-419", "es_419"),
|
||||
MangaPark("sw"),
|
||||
MangaPark("sv"),
|
||||
MangaPark("tg"),
|
||||
MangaPark("ta"),
|
||||
MangaPark("th"),
|
||||
MangaPark("ti"),
|
||||
MangaPark("to"),
|
||||
MangaPark("tr"),
|
||||
MangaPark("tk"),
|
||||
MangaPark("uk"),
|
||||
MangaPark("ur"),
|
||||
MangaPark("uz"),
|
||||
MangaPark("vi"),
|
||||
MangaPark("yo"),
|
||||
MangaPark("zu"),
|
||||
MangaPark("other", "_t"),
|
||||
)
|
||||
}
|
||||
|
||||
class LanguageOption(val lang: String, val siteLang: String = lang)
|
||||
private val languages = listOf(
|
||||
// LanguageOption("<Language Format>","<Language Format used in site.>"),
|
||||
LanguageOption("af"),
|
||||
LanguageOption("sq"),
|
||||
LanguageOption("am"),
|
||||
LanguageOption("ar"),
|
||||
LanguageOption("hy"),
|
||||
LanguageOption("az"),
|
||||
LanguageOption("be"),
|
||||
LanguageOption("bn"),
|
||||
LanguageOption("bs"),
|
||||
LanguageOption("bg"),
|
||||
LanguageOption("my"),
|
||||
LanguageOption("km"),
|
||||
LanguageOption("ca"),
|
||||
LanguageOption("ceb"),
|
||||
LanguageOption("zh"),
|
||||
LanguageOption("zh-Hans", "zh_hk"),
|
||||
LanguageOption("zh-Hant", "zh_tw"),
|
||||
LanguageOption("hr"),
|
||||
LanguageOption("cs"),
|
||||
LanguageOption("da"),
|
||||
LanguageOption("nl"),
|
||||
LanguageOption("en"),
|
||||
LanguageOption("eo"),
|
||||
LanguageOption("et"),
|
||||
LanguageOption("fo"),
|
||||
LanguageOption("fil"),
|
||||
LanguageOption("fi"),
|
||||
LanguageOption("fr"),
|
||||
LanguageOption("ka"),
|
||||
LanguageOption("de"),
|
||||
LanguageOption("el"),
|
||||
LanguageOption("gn"),
|
||||
LanguageOption("ht"),
|
||||
LanguageOption("ha"),
|
||||
LanguageOption("he"),
|
||||
LanguageOption("hi"),
|
||||
LanguageOption("hu"),
|
||||
LanguageOption("is"),
|
||||
LanguageOption("ig"),
|
||||
LanguageOption("id"),
|
||||
LanguageOption("ga"),
|
||||
LanguageOption("it"),
|
||||
LanguageOption("ja"),
|
||||
LanguageOption("jv"),
|
||||
LanguageOption("kk"),
|
||||
LanguageOption("ko"),
|
||||
LanguageOption("ku"),
|
||||
LanguageOption("ky"),
|
||||
LanguageOption("lo"),
|
||||
LanguageOption("lv"),
|
||||
LanguageOption("lt"),
|
||||
LanguageOption("lb"),
|
||||
LanguageOption("mk"),
|
||||
LanguageOption("mg"),
|
||||
LanguageOption("ms"),
|
||||
LanguageOption("ml"),
|
||||
LanguageOption("mt"),
|
||||
LanguageOption("mi"),
|
||||
LanguageOption("mo"),
|
||||
LanguageOption("mn"),
|
||||
LanguageOption("ne"),
|
||||
LanguageOption("no"),
|
||||
LanguageOption("ny"),
|
||||
LanguageOption("ps"),
|
||||
LanguageOption("fa"),
|
||||
LanguageOption("pl"),
|
||||
LanguageOption("pt"),
|
||||
LanguageOption("pt-BR", "pt_br"),
|
||||
LanguageOption("ro"),
|
||||
LanguageOption("rm"),
|
||||
LanguageOption("ru"),
|
||||
LanguageOption("sm"),
|
||||
LanguageOption("sr"),
|
||||
LanguageOption("sh"),
|
||||
LanguageOption("st"),
|
||||
LanguageOption("sn"),
|
||||
LanguageOption("sd"),
|
||||
LanguageOption("si"),
|
||||
LanguageOption("sk"),
|
||||
LanguageOption("sl"),
|
||||
LanguageOption("so"),
|
||||
LanguageOption("es"),
|
||||
LanguageOption("es-419", "es_419"),
|
||||
LanguageOption("sw"),
|
||||
LanguageOption("sv"),
|
||||
LanguageOption("tg"),
|
||||
LanguageOption("ta"),
|
||||
LanguageOption("th"),
|
||||
LanguageOption("ti"),
|
||||
LanguageOption("to"),
|
||||
LanguageOption("tr"),
|
||||
LanguageOption("tk"),
|
||||
LanguageOption("uk"),
|
||||
LanguageOption("ur"),
|
||||
LanguageOption("uz"),
|
||||
LanguageOption("vi"),
|
||||
LanguageOption("yo"),
|
||||
LanguageOption("zu"),
|
||||
LanguageOption("other", "_t"),
|
||||
)
|
||||
|
|
|
@ -2,300 +2,132 @@ package eu.kanade.tachiyomi.extension.all.mangapark
|
|||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class MangaParkFilters {
|
||||
|
||||
internal fun getFilterList(): FilterList {
|
||||
return FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
SortFilter("Sort By", defaultSort, sortList),
|
||||
Filter.Separator(),
|
||||
MinChapterFilter(),
|
||||
MaxChapterFilter(),
|
||||
Filter.Separator(),
|
||||
PublicationFilter("Status", publicationList, 0),
|
||||
TypeFilter("Type", typeList),
|
||||
DemographicFilter("Demographic", demographicList),
|
||||
ContentFilter("Content", contentList),
|
||||
GenreFilter("Genre", genreList),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList): String {
|
||||
var sort = "rating.za"
|
||||
var minChap: Int? = null
|
||||
var maxChap: Int? = null
|
||||
var publication: String? = null
|
||||
val includedGenre = mutableListOf<String>()
|
||||
val excludedGenre = mutableListOf<String>()
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> {
|
||||
val sortType = sortList[filter.state!!.index].value
|
||||
val sortDirection = if (filter.state!!.ascending) "az" else "za"
|
||||
sort = "$sortType.$sortDirection"
|
||||
}
|
||||
is MinChapterFilter -> {
|
||||
try {
|
||||
minChap = filter.state.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
// Do Nothing
|
||||
}
|
||||
}
|
||||
is MaxChapterFilter -> {
|
||||
try {
|
||||
maxChap = filter.state.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
// Do Nothing
|
||||
}
|
||||
}
|
||||
is PublicationFilter -> {
|
||||
if (filter.state != 0) {
|
||||
publication = publicationList[filter.state].value
|
||||
}
|
||||
}
|
||||
is TypeFilter -> {
|
||||
includedGenre += filter.state.filter { it.isIncluded() }.map { it.value }
|
||||
excludedGenre += filter.state.filter { it.isExcluded() }.map { it.value }
|
||||
}
|
||||
is DemographicFilter -> {
|
||||
includedGenre += filter.state.filter { it.isIncluded() }.map { it.value }
|
||||
excludedGenre += filter.state.filter { it.isExcluded() }.map { it.value }
|
||||
}
|
||||
is ContentFilter -> {
|
||||
includedGenre += filter.state.filter { it.isIncluded() }.map { it.value }
|
||||
excludedGenre += filter.state.filter { it.isExcluded() }.map { it.value }
|
||||
}
|
||||
is GenreFilter -> {
|
||||
includedGenre += filter.state.filter { it.isIncluded() }.map { it.value }
|
||||
excludedGenre += filter.state.filter { it.isExcluded() }.map { it.value }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return url.apply {
|
||||
if (sort != "rating.za") {
|
||||
addQueryParameter(
|
||||
"sort",
|
||||
sort,
|
||||
)
|
||||
}
|
||||
if (includedGenre.isNotEmpty() || excludedGenre.isNotEmpty()) {
|
||||
addQueryParameter(
|
||||
"genres",
|
||||
includedGenre.joinToString(",") + "|" + excludedGenre.joinToString(","),
|
||||
)
|
||||
}
|
||||
if (publication != null) {
|
||||
addQueryParameter(
|
||||
"release",
|
||||
publication,
|
||||
)
|
||||
}
|
||||
addQueryParameter(
|
||||
"chapters",
|
||||
minMaxToChapter(minChap, maxChap),
|
||||
)
|
||||
}.toString()
|
||||
}
|
||||
|
||||
private fun minMaxToChapter(minChap: Int?, maxChap: Int?): String? {
|
||||
if (minChap == null && maxChap == null) return null
|
||||
return when {
|
||||
minChap != null && maxChap == null -> minChap
|
||||
minChap == null && maxChap != null -> "0-$maxChap"
|
||||
else -> "$minChap-$maxChap"
|
||||
}.toString()
|
||||
}
|
||||
|
||||
// Sort Filter
|
||||
class SortItem(val name: String, val value: String)
|
||||
|
||||
private val sortList: List<SortItem> = listOf(
|
||||
SortItem("Rating", "rating"),
|
||||
SortItem("Comments", "comments"),
|
||||
SortItem("Discuss", "discuss"),
|
||||
SortItem("Update", "update"),
|
||||
SortItem("Create", "create"),
|
||||
SortItem("Name", "name"),
|
||||
SortItem("Total Views", "d000"),
|
||||
SortItem("Most Views 360 days", "d360"),
|
||||
SortItem("Most Views 180 days", "d180"),
|
||||
SortItem("Most Views 90 days", "d090"),
|
||||
SortItem("Most Views 30 days", "d030"),
|
||||
SortItem("Most Views 7 days", "d007"),
|
||||
SortItem("Most Views 24 hours", "h024"),
|
||||
SortItem("Most Views 12 hours", "h012"),
|
||||
SortItem("Most Views 6 hours", "h006"),
|
||||
SortItem("Most Views 60 minutes", "h001"),
|
||||
)
|
||||
|
||||
class SortDefault(val defaultSortIndex: Int, val ascending: Boolean)
|
||||
|
||||
private val defaultSort: SortDefault = SortDefault(0, false)
|
||||
|
||||
class SortFilter(name: String, default: SortDefault, sorts: List<SortItem>) :
|
||||
Filter.Sort(
|
||||
name,
|
||||
sorts.map { it.name }.toTypedArray(),
|
||||
Selection(default.defaultSortIndex, default.ascending),
|
||||
)
|
||||
|
||||
// Min - Max Chapter Filter
|
||||
abstract class TextFilter(name: String) : Filter.Text(name)
|
||||
|
||||
class MinChapterFilter : TextFilter("Min. Chapters")
|
||||
class MaxChapterFilter : TextFilter("Max. Chapters")
|
||||
|
||||
// Publication Filter
|
||||
class PublicationItem(val name: String, val value: String)
|
||||
|
||||
private val publicationList: List<PublicationItem> = listOf(
|
||||
PublicationItem("All", ""),
|
||||
PublicationItem("Pending", "pending"),
|
||||
PublicationItem("Ongoing", "ongoing"),
|
||||
PublicationItem("Completed", "completed"),
|
||||
PublicationItem("Hiatus", "hiatus"),
|
||||
PublicationItem("Cancelled", "cancelled"),
|
||||
)
|
||||
|
||||
class PublicationFilter(
|
||||
name: String,
|
||||
statusList: List<PublicationItem>,
|
||||
defaultStatusIndex: Int,
|
||||
) :
|
||||
Filter.Select<String>(
|
||||
name,
|
||||
statusList.map { it.name }.toTypedArray(),
|
||||
defaultStatusIndex,
|
||||
)
|
||||
|
||||
// Type
|
||||
class TypeItem(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
private val typeList: List<TypeItem> = listOf(
|
||||
TypeItem("Cartoon", "cartoon"),
|
||||
TypeItem("Comic", "comic"),
|
||||
TypeItem("Doujinshi", "doujinshi"),
|
||||
TypeItem("Manga", "manga"),
|
||||
TypeItem("Manhua", "manhua"),
|
||||
TypeItem("Manhwa", "manhwa"),
|
||||
TypeItem("Webtoon", "webtoon"),
|
||||
)
|
||||
|
||||
class TypeFilter(name: String, typeList: List<TypeItem>) :
|
||||
Filter.Group<TypeItem>(name, typeList)
|
||||
|
||||
// Demographic
|
||||
class DemographicItem(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
private val demographicList: List<DemographicItem> = listOf(
|
||||
DemographicItem("Shounen", "shounen"),
|
||||
DemographicItem("Shoujo", "shoujo"),
|
||||
DemographicItem("Seinen", "seinen"),
|
||||
DemographicItem("Josei", "josei"),
|
||||
)
|
||||
|
||||
class DemographicFilter(name: String, demographicList: List<DemographicItem>) :
|
||||
Filter.Group<DemographicItem>(name, demographicList)
|
||||
|
||||
// Content
|
||||
class ContentItem(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
private val contentList: List<ContentItem> = listOf(
|
||||
ContentItem("Adult", "adult"),
|
||||
ContentItem("Ecchi", "ecchi"),
|
||||
ContentItem("Gore", "gore"),
|
||||
ContentItem("Hentai", "hentai"),
|
||||
ContentItem("Mature", "mature"),
|
||||
ContentItem("Smut", "smut"),
|
||||
)
|
||||
|
||||
class ContentFilter(name: String, contentList: List<ContentItem>) :
|
||||
Filter.Group<ContentItem>(name, contentList)
|
||||
|
||||
// Genre
|
||||
class GenreItem(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
private val genreList: List<GenreItem> = listOf(
|
||||
GenreItem("Action", "action"),
|
||||
GenreItem("Adaptation", "adaptation"),
|
||||
GenreItem("Adventure", "adventure"),
|
||||
GenreItem("Aliens", "aliens"),
|
||||
GenreItem("Animals", "animals"),
|
||||
GenreItem("Anthology", "anthology"),
|
||||
GenreItem("Award Winning", "award_winning"), // This Is Hidden In Web
|
||||
GenreItem("Cars", "cars"),
|
||||
GenreItem("Comedy", "comedy"),
|
||||
GenreItem("Cooking", "cooking"),
|
||||
GenreItem("Crime", "crime"),
|
||||
GenreItem("Crossdressing", "crossdressing"),
|
||||
GenreItem("Delinquents", "delinquents"),
|
||||
GenreItem("Dementia", "dementia"),
|
||||
GenreItem("Demons", "demons"),
|
||||
GenreItem("Drama", "drama"),
|
||||
GenreItem("Fantasy", "fantasy"),
|
||||
GenreItem("Full Color", "full_color"),
|
||||
GenreItem("Game", "game"),
|
||||
GenreItem("Gender Bender", "gender_bender"),
|
||||
GenreItem("Genderswap", "genderswap"),
|
||||
GenreItem("Gyaru", "gyaru"),
|
||||
GenreItem("Harem", "harem"),
|
||||
GenreItem("Historical", "historical"),
|
||||
GenreItem("Horror", "horror"),
|
||||
GenreItem("Incest", "incest"),
|
||||
GenreItem("Isekai", "isekai"),
|
||||
GenreItem("Kids", "kids"),
|
||||
GenreItem("Loli", "loli"),
|
||||
GenreItem("Lolicon", "lolicon"),
|
||||
GenreItem("Magic", "magic"),
|
||||
GenreItem("Magical Girls", "magical_girls"),
|
||||
GenreItem("Martial Arts", "martial_arts"),
|
||||
GenreItem("Mecha", "mecha"),
|
||||
GenreItem("Medical", "medical"),
|
||||
GenreItem("Military", "military"),
|
||||
GenreItem("Monster Girls", "monster_girls"),
|
||||
GenreItem("Monsters", "monsters"),
|
||||
GenreItem("Music", "music"),
|
||||
GenreItem("Mystery", "mystery"),
|
||||
GenreItem("Office Workers", "office_workers"),
|
||||
GenreItem("Oneshot", "oneshot"),
|
||||
GenreItem("Parody", "parody"),
|
||||
GenreItem("Philosophical", "philosophical"),
|
||||
GenreItem("Police", "police"),
|
||||
GenreItem("Post Apocalyptic", "post_apocalyptic"),
|
||||
GenreItem("Psychological", "psychological"),
|
||||
GenreItem("Reincarnation", "reincarnation"),
|
||||
GenreItem("Romance", "romance"),
|
||||
GenreItem("Samurai", "samurai"),
|
||||
GenreItem("School Life", "school_life"),
|
||||
GenreItem("Sci-fi", "sci_fi"),
|
||||
GenreItem("Shotacon", "shotacon"),
|
||||
GenreItem("Shounen Ai", "shounen_ai"),
|
||||
GenreItem("Shoujo Ai", "shoujo_ai"),
|
||||
GenreItem("Slice of Life", "slice_of_life"),
|
||||
GenreItem("Space", "space"),
|
||||
GenreItem("Sports", "sports"),
|
||||
GenreItem("Super Power", "super_power"),
|
||||
GenreItem("Superhero", "superhero"),
|
||||
GenreItem("Supernatural", "supernatural"),
|
||||
GenreItem("Survival", "survival"),
|
||||
GenreItem("Thriller", "thriller"),
|
||||
GenreItem("Traditional Games", "traditional_games"),
|
||||
GenreItem("Tragedy", "tragedy"),
|
||||
GenreItem("Vampires", "vampires"),
|
||||
GenreItem("Video Games", "video_games"),
|
||||
GenreItem("Virtual Reality", "virtual_reality"),
|
||||
GenreItem("Wuxia", "wuxia"),
|
||||
GenreItem("Yaoi", "yaoi"),
|
||||
GenreItem("Yuri", "yuri"),
|
||||
GenreItem("Zombies", "zombies"),
|
||||
)
|
||||
|
||||
class GenreFilter(name: String, genreList: List<GenreItem>) :
|
||||
Filter.Group<GenreItem>(name, genreList)
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
defaultValue: String? = null,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||
) {
|
||||
val selected get() = options[state].second.takeUnless { it.isEmpty() }
|
||||
}
|
||||
|
||||
class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
abstract class CheckBoxGroup(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : Filter.Group<CheckBoxFilter>(
|
||||
name,
|
||||
options.map { CheckBoxFilter(it.first, it.second) },
|
||||
) {
|
||||
val checked get() = state.filter { it.state }.map { it.value }.takeUnless { it.isEmpty() }
|
||||
}
|
||||
|
||||
class TriStateFilter(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
abstract class TriStateGroup(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
) : Filter.Group<TriStateFilter>(
|
||||
name,
|
||||
options.map { TriStateFilter(it.first, it.second) },
|
||||
) {
|
||||
val included get() = state.filter { it.isIncluded() }.map { it.value }.takeUnless { it.isEmpty() }
|
||||
val excluded get() = state.filter { it.isExcluded() }.map { it.value }.takeUnless { it.isEmpty() }
|
||||
}
|
||||
|
||||
class SortFilter(defaultOrder: String? = null) : SelectFilter("Sort By", sort, defaultOrder) {
|
||||
companion object {
|
||||
private val sort = listOf(
|
||||
Pair("Rating Score", "field_score"),
|
||||
Pair("Most Follows", "field_follow"),
|
||||
Pair("Most Reviews", "field_review"),
|
||||
Pair("Most Comments", "field_comment"),
|
||||
Pair("Most Chapters", "field_chapter"),
|
||||
Pair("New Chapters", "field_update"),
|
||||
Pair("Recently Created", "field_create"),
|
||||
Pair("Name A-Z", "field_name"),
|
||||
Pair("Total Views", "views_d000"),
|
||||
Pair("Most Views 360 days", "views_d360"),
|
||||
Pair("Most Views 180 days", "views_d180"),
|
||||
Pair("Most Views 90 days", "views_d090"),
|
||||
Pair("Most Views 30 days", "views_d030"),
|
||||
Pair("Most Views 7 days", "views_d007"),
|
||||
Pair("Most Views 24 hours", "views_h024"),
|
||||
Pair("Most Views 12 hours", "views_h012"),
|
||||
Pair("Most Views 6 hours", "views_h006"),
|
||||
Pair("Most Views 60 minutes", "views_h001"),
|
||||
)
|
||||
|
||||
val POPULAR = FilterList(SortFilter("field_score"))
|
||||
val LATEST = FilterList(SortFilter("field_update"))
|
||||
}
|
||||
}
|
||||
|
||||
class GenreFilter(genres: List<Pair<String, String>>) : TriStateGroup("Genres", genres)
|
||||
|
||||
abstract class StatusFilter(name: String) : SelectFilter(name, status) {
|
||||
companion object {
|
||||
private val status = listOf(
|
||||
Pair("All", ""),
|
||||
Pair("Pending", "pending"),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Completed", "completed"),
|
||||
Pair("Hiatus", "hiatus"),
|
||||
Pair("Cancelled", "cancelled"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OriginalLanguageFilter : CheckBoxGroup("Original Work Language", language) {
|
||||
companion object {
|
||||
private val language = listOf(
|
||||
Pair("Chinese", "zh"),
|
||||
Pair("English", "en"),
|
||||
Pair("Japanese", "ja"),
|
||||
Pair("Korean", "ko"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OriginalStatusFilter : StatusFilter("Original Work Status")
|
||||
|
||||
class UploadStatusFilter : StatusFilter("Upload Status")
|
||||
|
||||
class ChapterCountFilter : SelectFilter("Number of Chapters", chapters) {
|
||||
companion object {
|
||||
private val chapters = listOf(
|
||||
Pair("", ""),
|
||||
Pair("0", "0"),
|
||||
Pair("1+", "1"),
|
||||
Pair("10+", "10"),
|
||||
Pair("20+", "20"),
|
||||
Pair("30+", "30"),
|
||||
Pair("40+", "40"),
|
||||
Pair("50+", "50"),
|
||||
Pair("60+", "60"),
|
||||
Pair("70+", "70"),
|
||||
Pair("80+", "80"),
|
||||
Pair("90+", "90"),
|
||||
Pair("100+", "100"),
|
||||
Pair("200+", "200"),
|
||||
Pair("300+", "300"),
|
||||
Pair("299~200", "200-299"),
|
||||
Pair("199~100", "100-199"),
|
||||
Pair("99~90", "90-99"),
|
||||
Pair("89~80", "80-89"),
|
||||
Pair("79~70", "70-79"),
|
||||
Pair("69~60", "60-69"),
|
||||
Pair("59~50", "50-59"),
|
||||
Pair("49~40", "40-49"),
|
||||
Pair("39~30", "30-39"),
|
||||
Pair("29~20", "20-29"),
|
||||
Pair("19~10", "10-19"),
|
||||
Pair("9~1", "1-9"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GraphQL<T>(
|
||||
val variables: T,
|
||||
val query: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchVariables(val select: SearchPayload)
|
||||
|
||||
@Serializable
|
||||
data class SearchPayload(
|
||||
@SerialName("word") val query: String? = null,
|
||||
val incGenres: List<String>? = null,
|
||||
val excGenres: List<String>? = null,
|
||||
val incTLangs: List<String>? = null,
|
||||
val incOLangs: List<String>? = null,
|
||||
val sortby: String? = null,
|
||||
val chapCount: String? = null,
|
||||
val origStatus: String? = null,
|
||||
val siteStatus: String? = null,
|
||||
val page: Int,
|
||||
val size: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class IdVariables(val id: String)
|
|
@ -0,0 +1,100 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||
|
||||
private fun buildQuery(queryAction: () -> String): String {
|
||||
return queryAction()
|
||||
.trimIndent()
|
||||
.replace("%", "$")
|
||||
}
|
||||
|
||||
val SEARCH_QUERY = buildQuery {
|
||||
"""
|
||||
query (
|
||||
%select: SearchComic_Select
|
||||
) {
|
||||
get_searchComic(
|
||||
select: %select
|
||||
) {
|
||||
items {
|
||||
data {
|
||||
id
|
||||
name
|
||||
altNames
|
||||
artists
|
||||
authors
|
||||
genres
|
||||
originalStatus
|
||||
uploadStatus
|
||||
summary
|
||||
urlCoverOri
|
||||
urlPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val DETAILS_QUERY = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%id: ID!
|
||||
) {
|
||||
get_comicNode(
|
||||
id: %id
|
||||
) {
|
||||
data {
|
||||
id
|
||||
name
|
||||
altNames
|
||||
artists
|
||||
authors
|
||||
genres
|
||||
originalStatus
|
||||
uploadStatus
|
||||
summary
|
||||
urlCoverOri
|
||||
urlPath
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val CHAPTERS_QUERY = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%id: ID!
|
||||
) {
|
||||
get_comicChapterList(
|
||||
comicId: %id
|
||||
) {
|
||||
data {
|
||||
id
|
||||
dname
|
||||
title
|
||||
dateModify
|
||||
dateCreate
|
||||
urlPath
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val PAGES_QUERY = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%id: ID!
|
||||
) {
|
||||
get_chapterNode(
|
||||
id: %id
|
||||
) {
|
||||
data {
|
||||
imageFile {
|
||||
urlList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MangaParkUrlActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val host = intent?.data?.host
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
||||
if (host != null && pathSegments != null) {
|
||||
val query = fromGuya(pathSegments)
|
||||
|
||||
if (query == null) {
|
||||
Log.e("MangaParkUrlActivity", "Unable to parse URI from intent $intent")
|
||||
finish()
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", query)
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("MangaParkUrlActivity", e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun fromGuya(pathSegments: MutableList<String>): String? {
|
||||
return if (pathSegments.size >= 2) {
|
||||
val id = pathSegments[1]
|
||||
"id:$id"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|