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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest />
|
||||||
<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>
|
|
||||||
|
|
|
@ -3,15 +3,11 @@ apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'MangaPark v3'
|
extName = 'MangaPark'
|
||||||
pkgNameSuffix = 'all.mangapark'
|
pkgNameSuffix = 'all.mangapark'
|
||||||
extClass = '.MangaParkFactory'
|
extClass = '.MangaParkFactory'
|
||||||
extVersionCode = 18
|
extVersionCode = 19
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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
|
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
import android.app.Application
|
||||||
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
|
import android.widget.Toast
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import uy.kohesive.injekt.Injekt
|
||||||
import org.jsoup.nodes.Element
|
import uy.kohesive.injekt.api.get
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
open class MangaPark(
|
class MangaPark(
|
||||||
final override val lang: String,
|
override val lang: String,
|
||||||
private val siteLang: String,
|
private val siteLang: String = lang,
|
||||||
) : ParsedHttpSource() {
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val name: String = "MangaPark v3"
|
override val name = "MangaPark"
|
||||||
|
|
||||||
override val baseUrl: String = "https://mangapark.net"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val id: Long = when (lang) {
|
override val versionId = 2
|
||||||
"zh-Hans" -> 6306867705763005424
|
|
||||||
"zh-Hant" -> 4563855043528673539
|
private val preference =
|
||||||
else -> super.id
|
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 json: Json by injectLazy()
|
||||||
|
|
||||||
private val mpFilters = MangaParkFilters()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.addInterceptor(CookieInterceptor(domain, "nsfw", "2"))
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
.rateLimitHost(apiUrl.toHttpUrl(), 1)
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Site Browse Helper
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
private fun browseMangaSelector(): String = "div#subject-list div.col"
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
private fun browseNextPageSelector(): String =
|
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
|
||||||
"div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)"
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
private fun browseMangaFromElement(element: Element): SManga {
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
|
||||||
return SManga.create().apply {
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latest
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
val payload = GraphQL(
|
||||||
GET("$baseUrl/browse?sort=update&page=$page")
|
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()
|
return POST(apiUrl, headers, payload)
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
runCatching(::getGenres)
|
||||||
val isBrowse = response.request.url.pathSegments[0] == "browse"
|
|
||||||
val mangaSelector = if (isBrowse) browseMangaSelector() else searchMangaSelector()
|
|
||||||
val nextPageSelector = if (isBrowse) browseNextPageSelector() else searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
val mangas = document.select(mangaSelector).map { element ->
|
val result = response.parseAs<SearchResponse>()
|
||||||
if (isBrowse) browseMangaFromElement(element) else searchMangaFromElement(element)
|
|
||||||
|
val entries = result.data.searchComics.items.map { it.data.toSManga() }
|
||||||
|
val hasNextPage = entries.size == size
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasNextPage = document.select(nextPageSelector).first() != null
|
private var genreCache: List<Pair<String, String>> = emptyList()
|
||||||
|
private var genreFetchAttempt = 0
|
||||||
|
|
||||||
return MangasPage(mangas, hasNextPage)
|
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()
|
||||||
|
|
||||||
|
genreCache = elements.mapNotNull {
|
||||||
|
val name = it.selectFirst("span.whitespace-nowrap")
|
||||||
|
?.text()?.takeUnless(String::isEmpty)
|
||||||
|
?: return@mapNotNull null
|
||||||
|
|
||||||
|
val key = it.attr("q:key")
|
||||||
|
.takeUnless(String::isEmpty) ?: return@mapNotNull null
|
||||||
|
|
||||||
|
Pair(name, key)
|
||||||
}
|
}
|
||||||
|
genreFetchAttempt++
|
||||||
// Manga Details
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
val infoElement = document.select("div#mainer div.container-fluid")
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(infoElement.select("h3.item-title a").attr("href"))
|
|
||||||
|
|
||||||
title = infoElement.select("h3.item-title").text()
|
|
||||||
|
|
||||||
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() }
|
|
||||||
|
|
||||||
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() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String?.parseStatus() = if (this == null) {
|
override fun getFilterList(): FilterList {
|
||||||
SManga.UNKNOWN
|
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 {
|
} else {
|
||||||
when {
|
filters.addAll(1, listOf(GenreFilter(genreCache)))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val chapterListHtml = response.asJsoup().select("div.episode-list #chap-index")
|
val result = response.parseAs<ChapterListResponse>()
|
||||||
return chapterListHtml.flatMap { it.select(chapterListSelector()).map { chapElem -> chapterFromElement(chapElem) } }
|
|
||||||
|
return result.data.chapterList.map { it.data.toSChapter() }.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListSelector(): String {
|
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
||||||
return when (lang) {
|
|
||||||
"en" -> "div.p-2:not(:has(.px-3))"
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
// To handle both "/comic/1/test/c0-en" and "/comic/1/test/c0-en/" like url
|
val payload = GraphQL(
|
||||||
else -> "div.p-2:has(.px-3 a[href\$=\"$siteLang\"]), div.p-2:has(.px-3 a[href\$=\"$siteLang/\"])"
|
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 {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
val urlElement = element.select("a.ms-3")
|
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 {
|
setOnPreferenceChangeListener { _, _ ->
|
||||||
name = urlElement.text().removePrefix("Ch").trim()
|
Toast.makeText(screen.context, "Restart Tachiyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||||
date_upload = element.select("i.text-nowrap").text().parseChapterDate()
|
true
|
||||||
setUrlWithoutDomain(urlElement.attr("href").removeSuffix("/"))
|
|
||||||
}
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String?.parseChapterDate(): Long {
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
if (this == null) return 0L
|
use { body.string() }.let(json::decodeFromString)
|
||||||
val value = this.split(' ')[0].toInt()
|
|
||||||
|
|
||||||
return when (this.split(' ')[1].removeSuffix("s")) {
|
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
|
||||||
"sec" -> Calendar.getInstance().apply {
|
filterIsInstance<T>().firstOrNull()
|
||||||
add(Calendar.SECOND, value * -1)
|
|
||||||
}.timeInMillis
|
private inline fun <reified T : Any> T.toJsonRequestBody() =
|
||||||
"min" -> Calendar.getInstance().apply {
|
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
add(Calendar.MINUTE, value * -1)
|
|
||||||
}.timeInMillis
|
override fun imageUrlParse(response: Response): String {
|
||||||
"hour" -> Calendar.getInstance().apply {
|
throw UnsupportedOperationException("Not Used")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val script = document.selectFirst("script:containsData(imgHttpLis):containsData(amWord):containsData(amPass)")?.html()
|
|
||||||
?: throw RuntimeException("Couldn't find script with image data.")
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
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 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 {
|
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
|
package eu.kanade.tachiyomi.extension.all.mangapark
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
class MangaParkFactory : SourceFactory {
|
class MangaParkFactory : SourceFactory {
|
||||||
override fun createSources(): List<Source> = languages.map { MangaPark(it.lang, it.siteLang) }
|
override fun createSources() = listOf(
|
||||||
}
|
MangaPark("af"),
|
||||||
|
MangaPark("sq"),
|
||||||
class LanguageOption(val lang: String, val siteLang: String = lang)
|
MangaPark("am"),
|
||||||
private val languages = listOf(
|
MangaPark("ar"),
|
||||||
// LanguageOption("<Language Format>","<Language Format used in site.>"),
|
MangaPark("hy"),
|
||||||
LanguageOption("af"),
|
MangaPark("az"),
|
||||||
LanguageOption("sq"),
|
MangaPark("be"),
|
||||||
LanguageOption("am"),
|
MangaPark("bn"),
|
||||||
LanguageOption("ar"),
|
MangaPark("bs"),
|
||||||
LanguageOption("hy"),
|
MangaPark("bg"),
|
||||||
LanguageOption("az"),
|
MangaPark("my"),
|
||||||
LanguageOption("be"),
|
MangaPark("km"),
|
||||||
LanguageOption("bn"),
|
MangaPark("ca"),
|
||||||
LanguageOption("bs"),
|
MangaPark("ceb"),
|
||||||
LanguageOption("bg"),
|
MangaPark("zh"),
|
||||||
LanguageOption("my"),
|
MangaPark("zh-Hans", "zh_hk"),
|
||||||
LanguageOption("km"),
|
MangaPark("zh-Hant", "zh_tw"),
|
||||||
LanguageOption("ca"),
|
MangaPark("hr"),
|
||||||
LanguageOption("ceb"),
|
MangaPark("cs"),
|
||||||
LanguageOption("zh"),
|
MangaPark("da"),
|
||||||
LanguageOption("zh-Hans", "zh_hk"),
|
MangaPark("nl"),
|
||||||
LanguageOption("zh-Hant", "zh_tw"),
|
MangaPark("en"),
|
||||||
LanguageOption("hr"),
|
MangaPark("eo"),
|
||||||
LanguageOption("cs"),
|
MangaPark("et"),
|
||||||
LanguageOption("da"),
|
MangaPark("fo"),
|
||||||
LanguageOption("nl"),
|
MangaPark("fil"),
|
||||||
LanguageOption("en"),
|
MangaPark("fi"),
|
||||||
LanguageOption("eo"),
|
MangaPark("fr"),
|
||||||
LanguageOption("et"),
|
MangaPark("ka"),
|
||||||
LanguageOption("fo"),
|
MangaPark("de"),
|
||||||
LanguageOption("fil"),
|
MangaPark("el"),
|
||||||
LanguageOption("fi"),
|
MangaPark("gn"),
|
||||||
LanguageOption("fr"),
|
MangaPark("ht"),
|
||||||
LanguageOption("ka"),
|
MangaPark("ha"),
|
||||||
LanguageOption("de"),
|
MangaPark("he"),
|
||||||
LanguageOption("el"),
|
MangaPark("hi"),
|
||||||
LanguageOption("gn"),
|
MangaPark("hu"),
|
||||||
LanguageOption("ht"),
|
MangaPark("is"),
|
||||||
LanguageOption("ha"),
|
MangaPark("ig"),
|
||||||
LanguageOption("he"),
|
MangaPark("id"),
|
||||||
LanguageOption("hi"),
|
MangaPark("ga"),
|
||||||
LanguageOption("hu"),
|
MangaPark("it"),
|
||||||
LanguageOption("is"),
|
MangaPark("ja"),
|
||||||
LanguageOption("ig"),
|
MangaPark("jv"),
|
||||||
LanguageOption("id"),
|
MangaPark("kk"),
|
||||||
LanguageOption("ga"),
|
MangaPark("ko"),
|
||||||
LanguageOption("it"),
|
MangaPark("ku"),
|
||||||
LanguageOption("ja"),
|
MangaPark("ky"),
|
||||||
LanguageOption("jv"),
|
MangaPark("lo"),
|
||||||
LanguageOption("kk"),
|
MangaPark("lv"),
|
||||||
LanguageOption("ko"),
|
MangaPark("lt"),
|
||||||
LanguageOption("ku"),
|
MangaPark("lb"),
|
||||||
LanguageOption("ky"),
|
MangaPark("mk"),
|
||||||
LanguageOption("lo"),
|
MangaPark("mg"),
|
||||||
LanguageOption("lv"),
|
MangaPark("ms"),
|
||||||
LanguageOption("lt"),
|
MangaPark("ml"),
|
||||||
LanguageOption("lb"),
|
MangaPark("mt"),
|
||||||
LanguageOption("mk"),
|
MangaPark("mi"),
|
||||||
LanguageOption("mg"),
|
MangaPark("mo"),
|
||||||
LanguageOption("ms"),
|
MangaPark("mn"),
|
||||||
LanguageOption("ml"),
|
MangaPark("ne"),
|
||||||
LanguageOption("mt"),
|
MangaPark("no"),
|
||||||
LanguageOption("mi"),
|
MangaPark("ny"),
|
||||||
LanguageOption("mo"),
|
MangaPark("ps"),
|
||||||
LanguageOption("mn"),
|
MangaPark("fa"),
|
||||||
LanguageOption("ne"),
|
MangaPark("pl"),
|
||||||
LanguageOption("no"),
|
MangaPark("pt"),
|
||||||
LanguageOption("ny"),
|
MangaPark("pt-BR", "pt_br"),
|
||||||
LanguageOption("ps"),
|
MangaPark("ro"),
|
||||||
LanguageOption("fa"),
|
MangaPark("rm"),
|
||||||
LanguageOption("pl"),
|
MangaPark("ru"),
|
||||||
LanguageOption("pt"),
|
MangaPark("sm"),
|
||||||
LanguageOption("pt-BR", "pt_br"),
|
MangaPark("sr"),
|
||||||
LanguageOption("ro"),
|
MangaPark("sh"),
|
||||||
LanguageOption("rm"),
|
MangaPark("st"),
|
||||||
LanguageOption("ru"),
|
MangaPark("sn"),
|
||||||
LanguageOption("sm"),
|
MangaPark("sd"),
|
||||||
LanguageOption("sr"),
|
MangaPark("si"),
|
||||||
LanguageOption("sh"),
|
MangaPark("sk"),
|
||||||
LanguageOption("st"),
|
MangaPark("sl"),
|
||||||
LanguageOption("sn"),
|
MangaPark("so"),
|
||||||
LanguageOption("sd"),
|
MangaPark("es"),
|
||||||
LanguageOption("si"),
|
MangaPark("es-419", "es_419"),
|
||||||
LanguageOption("sk"),
|
MangaPark("sw"),
|
||||||
LanguageOption("sl"),
|
MangaPark("sv"),
|
||||||
LanguageOption("so"),
|
MangaPark("tg"),
|
||||||
LanguageOption("es"),
|
MangaPark("ta"),
|
||||||
LanguageOption("es-419", "es_419"),
|
MangaPark("th"),
|
||||||
LanguageOption("sw"),
|
MangaPark("ti"),
|
||||||
LanguageOption("sv"),
|
MangaPark("to"),
|
||||||
LanguageOption("tg"),
|
MangaPark("tr"),
|
||||||
LanguageOption("ta"),
|
MangaPark("tk"),
|
||||||
LanguageOption("th"),
|
MangaPark("uk"),
|
||||||
LanguageOption("ti"),
|
MangaPark("ur"),
|
||||||
LanguageOption("to"),
|
MangaPark("uz"),
|
||||||
LanguageOption("tr"),
|
MangaPark("vi"),
|
||||||
LanguageOption("tk"),
|
MangaPark("yo"),
|
||||||
LanguageOption("uk"),
|
MangaPark("zu"),
|
||||||
LanguageOption("ur"),
|
MangaPark("other", "_t"),
|
||||||
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
class MangaParkFilters {
|
abstract class SelectFilter(
|
||||||
|
|
||||||
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,
|
name: String,
|
||||||
statusList: List<PublicationItem>,
|
private val options: List<Pair<String, String>>,
|
||||||
defaultStatusIndex: Int,
|
defaultValue: String? = null,
|
||||||
) :
|
) : Filter.Select<String>(
|
||||||
Filter.Select<String>(
|
|
||||||
name,
|
name,
|
||||||
statusList.map { it.name }.toTypedArray(),
|
options.map { it.first }.toTypedArray(),
|
||||||
defaultStatusIndex,
|
options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||||
)
|
) {
|
||||||
|
val selected get() = options[state].second.takeUnless { it.isEmpty() }
|
||||||
// Type
|
}
|
||||||
class TypeItem(name: String, val value: String) : Filter.TriState(name)
|
|
||||||
|
class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||||
private val typeList: List<TypeItem> = listOf(
|
|
||||||
TypeItem("Cartoon", "cartoon"),
|
abstract class CheckBoxGroup(
|
||||||
TypeItem("Comic", "comic"),
|
name: String,
|
||||||
TypeItem("Doujinshi", "doujinshi"),
|
options: List<Pair<String, String>>,
|
||||||
TypeItem("Manga", "manga"),
|
) : Filter.Group<CheckBoxFilter>(
|
||||||
TypeItem("Manhua", "manhua"),
|
name,
|
||||||
TypeItem("Manhwa", "manhwa"),
|
options.map { CheckBoxFilter(it.first, it.second) },
|
||||||
TypeItem("Webtoon", "webtoon"),
|
) {
|
||||||
)
|
val checked get() = state.filter { it.state }.map { it.value }.takeUnless { it.isEmpty() }
|
||||||
|
}
|
||||||
class TypeFilter(name: String, typeList: List<TypeItem>) :
|
|
||||||
Filter.Group<TypeItem>(name, typeList)
|
class TriStateFilter(name: String, val value: String) : Filter.TriState(name)
|
||||||
|
|
||||||
// Demographic
|
abstract class TriStateGroup(
|
||||||
class DemographicItem(name: String, val value: String) : Filter.TriState(name)
|
name: String,
|
||||||
|
private val options: List<Pair<String, String>>,
|
||||||
private val demographicList: List<DemographicItem> = listOf(
|
) : Filter.Group<TriStateFilter>(
|
||||||
DemographicItem("Shounen", "shounen"),
|
name,
|
||||||
DemographicItem("Shoujo", "shoujo"),
|
options.map { TriStateFilter(it.first, it.second) },
|
||||||
DemographicItem("Seinen", "seinen"),
|
) {
|
||||||
DemographicItem("Josei", "josei"),
|
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 DemographicFilter(name: String, demographicList: List<DemographicItem>) :
|
|
||||||
Filter.Group<DemographicItem>(name, demographicList)
|
class SortFilter(defaultOrder: String? = null) : SelectFilter("Sort By", sort, defaultOrder) {
|
||||||
|
companion object {
|
||||||
// Content
|
private val sort = listOf(
|
||||||
class ContentItem(name: String, val value: String) : Filter.TriState(name)
|
Pair("Rating Score", "field_score"),
|
||||||
|
Pair("Most Follows", "field_follow"),
|
||||||
private val contentList: List<ContentItem> = listOf(
|
Pair("Most Reviews", "field_review"),
|
||||||
ContentItem("Adult", "adult"),
|
Pair("Most Comments", "field_comment"),
|
||||||
ContentItem("Ecchi", "ecchi"),
|
Pair("Most Chapters", "field_chapter"),
|
||||||
ContentItem("Gore", "gore"),
|
Pair("New Chapters", "field_update"),
|
||||||
ContentItem("Hentai", "hentai"),
|
Pair("Recently Created", "field_create"),
|
||||||
ContentItem("Mature", "mature"),
|
Pair("Name A-Z", "field_name"),
|
||||||
ContentItem("Smut", "smut"),
|
Pair("Total Views", "views_d000"),
|
||||||
)
|
Pair("Most Views 360 days", "views_d360"),
|
||||||
|
Pair("Most Views 180 days", "views_d180"),
|
||||||
class ContentFilter(name: String, contentList: List<ContentItem>) :
|
Pair("Most Views 90 days", "views_d090"),
|
||||||
Filter.Group<ContentItem>(name, contentList)
|
Pair("Most Views 30 days", "views_d030"),
|
||||||
|
Pair("Most Views 7 days", "views_d007"),
|
||||||
// Genre
|
Pair("Most Views 24 hours", "views_h024"),
|
||||||
class GenreItem(name: String, val value: String) : Filter.TriState(name)
|
Pair("Most Views 12 hours", "views_h012"),
|
||||||
|
Pair("Most Views 6 hours", "views_h006"),
|
||||||
private val genreList: List<GenreItem> = listOf(
|
Pair("Most Views 60 minutes", "views_h001"),
|
||||||
GenreItem("Action", "action"),
|
)
|
||||||
GenreItem("Adaptation", "adaptation"),
|
|
||||||
GenreItem("Adventure", "adventure"),
|
val POPULAR = FilterList(SortFilter("field_score"))
|
||||||
GenreItem("Aliens", "aliens"),
|
val LATEST = FilterList(SortFilter("field_update"))
|
||||||
GenreItem("Animals", "animals"),
|
}
|
||||||
GenreItem("Anthology", "anthology"),
|
}
|
||||||
GenreItem("Award Winning", "award_winning"), // This Is Hidden In Web
|
|
||||||
GenreItem("Cars", "cars"),
|
class GenreFilter(genres: List<Pair<String, String>>) : TriStateGroup("Genres", genres)
|
||||||
GenreItem("Comedy", "comedy"),
|
|
||||||
GenreItem("Cooking", "cooking"),
|
abstract class StatusFilter(name: String) : SelectFilter(name, status) {
|
||||||
GenreItem("Crime", "crime"),
|
companion object {
|
||||||
GenreItem("Crossdressing", "crossdressing"),
|
private val status = listOf(
|
||||||
GenreItem("Delinquents", "delinquents"),
|
Pair("All", ""),
|
||||||
GenreItem("Dementia", "dementia"),
|
Pair("Pending", "pending"),
|
||||||
GenreItem("Demons", "demons"),
|
Pair("Ongoing", "ongoing"),
|
||||||
GenreItem("Drama", "drama"),
|
Pair("Completed", "completed"),
|
||||||
GenreItem("Fantasy", "fantasy"),
|
Pair("Hiatus", "hiatus"),
|
||||||
GenreItem("Full Color", "full_color"),
|
Pair("Cancelled", "cancelled"),
|
||||||
GenreItem("Game", "game"),
|
)
|
||||||
GenreItem("Gender Bender", "gender_bender"),
|
}
|
||||||
GenreItem("Genderswap", "genderswap"),
|
}
|
||||||
GenreItem("Gyaru", "gyaru"),
|
|
||||||
GenreItem("Harem", "harem"),
|
class OriginalLanguageFilter : CheckBoxGroup("Original Work Language", language) {
|
||||||
GenreItem("Historical", "historical"),
|
companion object {
|
||||||
GenreItem("Horror", "horror"),
|
private val language = listOf(
|
||||||
GenreItem("Incest", "incest"),
|
Pair("Chinese", "zh"),
|
||||||
GenreItem("Isekai", "isekai"),
|
Pair("English", "en"),
|
||||||
GenreItem("Kids", "kids"),
|
Pair("Japanese", "ja"),
|
||||||
GenreItem("Loli", "loli"),
|
Pair("Korean", "ko"),
|
||||||
GenreItem("Lolicon", "lolicon"),
|
)
|
||||||
GenreItem("Magic", "magic"),
|
}
|
||||||
GenreItem("Magical Girls", "magical_girls"),
|
}
|
||||||
GenreItem("Martial Arts", "martial_arts"),
|
|
||||||
GenreItem("Mecha", "mecha"),
|
class OriginalStatusFilter : StatusFilter("Original Work Status")
|
||||||
GenreItem("Medical", "medical"),
|
|
||||||
GenreItem("Military", "military"),
|
class UploadStatusFilter : StatusFilter("Upload Status")
|
||||||
GenreItem("Monster Girls", "monster_girls"),
|
|
||||||
GenreItem("Monsters", "monsters"),
|
class ChapterCountFilter : SelectFilter("Number of Chapters", chapters) {
|
||||||
GenreItem("Music", "music"),
|
companion object {
|
||||||
GenreItem("Mystery", "mystery"),
|
private val chapters = listOf(
|
||||||
GenreItem("Office Workers", "office_workers"),
|
Pair("", ""),
|
||||||
GenreItem("Oneshot", "oneshot"),
|
Pair("0", "0"),
|
||||||
GenreItem("Parody", "parody"),
|
Pair("1+", "1"),
|
||||||
GenreItem("Philosophical", "philosophical"),
|
Pair("10+", "10"),
|
||||||
GenreItem("Police", "police"),
|
Pair("20+", "20"),
|
||||||
GenreItem("Post Apocalyptic", "post_apocalyptic"),
|
Pair("30+", "30"),
|
||||||
GenreItem("Psychological", "psychological"),
|
Pair("40+", "40"),
|
||||||
GenreItem("Reincarnation", "reincarnation"),
|
Pair("50+", "50"),
|
||||||
GenreItem("Romance", "romance"),
|
Pair("60+", "60"),
|
||||||
GenreItem("Samurai", "samurai"),
|
Pair("70+", "70"),
|
||||||
GenreItem("School Life", "school_life"),
|
Pair("80+", "80"),
|
||||||
GenreItem("Sci-fi", "sci_fi"),
|
Pair("90+", "90"),
|
||||||
GenreItem("Shotacon", "shotacon"),
|
Pair("100+", "100"),
|
||||||
GenreItem("Shounen Ai", "shounen_ai"),
|
Pair("200+", "200"),
|
||||||
GenreItem("Shoujo Ai", "shoujo_ai"),
|
Pair("300+", "300"),
|
||||||
GenreItem("Slice of Life", "slice_of_life"),
|
Pair("299~200", "200-299"),
|
||||||
GenreItem("Space", "space"),
|
Pair("199~100", "100-199"),
|
||||||
GenreItem("Sports", "sports"),
|
Pair("99~90", "90-99"),
|
||||||
GenreItem("Super Power", "super_power"),
|
Pair("89~80", "80-89"),
|
||||||
GenreItem("Superhero", "superhero"),
|
Pair("79~70", "70-79"),
|
||||||
GenreItem("Supernatural", "supernatural"),
|
Pair("69~60", "60-69"),
|
||||||
GenreItem("Survival", "survival"),
|
Pair("59~50", "50-59"),
|
||||||
GenreItem("Thriller", "thriller"),
|
Pair("49~40", "40-49"),
|
||||||
GenreItem("Traditional Games", "traditional_games"),
|
Pair("39~30", "30-39"),
|
||||||
GenreItem("Tragedy", "tragedy"),
|
Pair("29~20", "20-29"),
|
||||||
GenreItem("Vampires", "vampires"),
|
Pair("19~10", "10-19"),
|
||||||
GenreItem("Video Games", "video_games"),
|
Pair("9~1", "1-9"),
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|