Add Manga Planet (#1319)
This commit is contained in:
parent
42ad2a5859
commit
bf53943984
12
src/en/mangaplanet/build.gradle
Normal file
12
src/en/mangaplanet/build.gradle
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
ext {
|
||||||
|
extName = "Manga Planet"
|
||||||
|
extClass = ".MangaPlanet"
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:speedbinb"))
|
||||||
|
}
|
BIN
src/en/mangaplanet/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/mangaplanet/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
src/en/mangaplanet/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/mangaplanet/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
src/en/mangaplanet/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/mangaplanet/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
src/en/mangaplanet/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/mangaplanet/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
src/en/mangaplanet/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/mangaplanet/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.mangaplanet
|
||||||
|
|
||||||
|
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("MangaPlanet", "failed to set cookie", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.mangaplanet
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
interface UrlFilter {
|
||||||
|
fun addToUrl(builder: HttpUrl.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortFilter : SelectFilter(
|
||||||
|
"Sort order",
|
||||||
|
"sort",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Released last", ""),
|
||||||
|
Pair("Released first", "1"),
|
||||||
|
Pair("By A to Z", "2"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class CategoryFilter : MultiSelectFilter(
|
||||||
|
"Category",
|
||||||
|
"cat",
|
||||||
|
listOf(
|
||||||
|
MultiSelectOption("Shojo/Josei", "3"),
|
||||||
|
MultiSelectOption("Shonen/Seinen", "1"),
|
||||||
|
MultiSelectOption("BL(futekiya)", "2"),
|
||||||
|
MultiSelectOption("GL/Yuri", "4"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SpicyLevelFilter : MultiSelectFilter(
|
||||||
|
"Spicy Level - BL(futekiya) only",
|
||||||
|
"hp",
|
||||||
|
listOf(
|
||||||
|
MultiSelectOption("🌶️🌶️🌶️🌶️🌶️", "5"),
|
||||||
|
MultiSelectOption("🌶️🌶️🌶️🌶️️", "4"),
|
||||||
|
MultiSelectOption("🌶️🌶️🌶️", "3"),
|
||||||
|
MultiSelectOption("🌶️🌶️", "2"),
|
||||||
|
MultiSelectOption("🌶️", "1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class AccessTypeFilter : SelectFilter(
|
||||||
|
"Access Type",
|
||||||
|
"bt",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("Access for free", "1"),
|
||||||
|
Pair("Access via Points", "2"),
|
||||||
|
Pair("Access via Manga Planet Pass", "3"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class FormatFilter : MultiSelectFilter(
|
||||||
|
"Format",
|
||||||
|
"fmt",
|
||||||
|
listOf(
|
||||||
|
MultiSelectOption("Manga", "1"),
|
||||||
|
MultiSelectOption("TatéManga", "2"),
|
||||||
|
MultiSelectOption("Novel", "3"), // Novels are images with text
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class RatingFilter : MultiSelectFilter(
|
||||||
|
"Rating",
|
||||||
|
"rtg",
|
||||||
|
listOf(
|
||||||
|
MultiSelectOption("All Ages", "0"),
|
||||||
|
MultiSelectOption("R16+", "16"),
|
||||||
|
MultiSelectOption("R18+", "18"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class ReleaseStatusFilter : SelectFilter(
|
||||||
|
"Release status",
|
||||||
|
"comp",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("Ongoing", "progress"),
|
||||||
|
Pair("Completed", "comp"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class LetterFilter : SelectFilter(
|
||||||
|
"Display by First Letter",
|
||||||
|
"fl",
|
||||||
|
buildList {
|
||||||
|
add(Pair("All", ""))
|
||||||
|
|
||||||
|
for (letter in 'A'..'Z') {
|
||||||
|
add(Pair(letter.toString(), letter.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(Pair("Other", "other"))
|
||||||
|
}
|
||||||
|
.toTypedArray(),
|
||||||
|
)
|
||||||
|
|
||||||
|
open class MultiSelectFilter(
|
||||||
|
name: String,
|
||||||
|
val queryParameter: String,
|
||||||
|
options: List<MultiSelectOption>,
|
||||||
|
) : Filter.Group<MultiSelectOption>(name, options), UrlFilter {
|
||||||
|
override fun addToUrl(builder: HttpUrl.Builder) {
|
||||||
|
val enabled = state.filter { it.state }
|
||||||
|
|
||||||
|
if (enabled.isEmpty() || enabled.size == state.size) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addQueryParameter(
|
||||||
|
queryParameter,
|
||||||
|
enabled.joinToString(",") { it.value },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiSelectOption(name: String, val value: String, state: Boolean = false) : Filter.CheckBox(name, state)
|
||||||
|
|
||||||
|
open class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
val queryParameter: String,
|
||||||
|
val vals: Array<Pair<String, String>>,
|
||||||
|
state: Int = 0,
|
||||||
|
) : Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state), UrlFilter {
|
||||||
|
override fun addToUrl(builder: HttpUrl.Builder) {
|
||||||
|
if (state == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addQueryParameter(queryParameter, vals[state].second)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.mangaplanet
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbInterceptor
|
||||||
|
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbReader
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
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.util.asJsoup
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MangaPlanet : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val name = "Manga Planet"
|
||||||
|
|
||||||
|
override val baseUrl = "https://mangaplanet.com"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
// No need to be lazy if you're going to use it immediately below.
|
||||||
|
private val json = Injekt.get<Json>()
|
||||||
|
|
||||||
|
override val client = network.client.newBuilder()
|
||||||
|
.addInterceptor(SpeedBinbInterceptor(json))
|
||||||
|
.addInterceptor(CookieInterceptor(baseUrl.toHttpUrl().host, "mpaconf", "18"))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/browse/title?ttlpage=$page", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = ".book-list"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||||
|
title = element.selectFirst("h3")!!.text()
|
||||||
|
author = element.selectFirst("p:has(.fa-pen-nib)")?.text()
|
||||||
|
description = element.selectFirst("h3 + p")?.text()
|
||||||
|
thumbnail_url = element.selectFirst("img")?.absUrl("data-src")
|
||||||
|
status = when {
|
||||||
|
element.selectFirst(".fa-flag-alt") != null -> SManga.COMPLETED
|
||||||
|
element.selectFirst(".fa-arrow-right") != null -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "ul.pagination a.page-link[rel=next]"
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
addPathSegment("search")
|
||||||
|
addQueryParameter("keyword", query)
|
||||||
|
} else {
|
||||||
|
addPathSegments("browse/title")
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.ifEmpty { getFilterList() }
|
||||||
|
.filterIsInstance<UrlFilter>()
|
||||||
|
.forEach { it.addToUrl(this) }
|
||||||
|
|
||||||
|
if (page > 1) {
|
||||||
|
addQueryParameter("ttlpage", page.toString())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val alternativeTitles = document.selectFirst("h3#manga_title + p")!!
|
||||||
|
.textNodes()
|
||||||
|
.filterNot { it.text().isBlank() }
|
||||||
|
.joinToString { it.text() }
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = document.selectFirst("h3#manga_title")!!.text()
|
||||||
|
author = document.select("h3:has(.fa-pen-nib) a").joinToString { it.text() }
|
||||||
|
description = buildString {
|
||||||
|
append("Alternative Titles: ")
|
||||||
|
appendLine(alternativeTitles)
|
||||||
|
appendLine()
|
||||||
|
appendLine(document.selectFirst("h3#manga_title ~ p:eq(2)")!!.text())
|
||||||
|
}
|
||||||
|
genre = buildList {
|
||||||
|
document.select("h3:has(.fa-layer-group) a")
|
||||||
|
.map { it.text() }
|
||||||
|
.let { addAll(it) }
|
||||||
|
document.select(".fa-pepper-hot").size
|
||||||
|
.takeIf { it > 0 }
|
||||||
|
?.let { add("🌶️".repeat(it)) }
|
||||||
|
document.select(".tags-btn button")
|
||||||
|
.map { it.text() }
|
||||||
|
.let { addAll(it) }
|
||||||
|
document.selectFirst("span:has(.fa-book-spells, .fa-book)")?.let { add(it.text()) }
|
||||||
|
document.selectFirst("span:has(.fa-user-friends)")?.let { add(it.text()) }
|
||||||
|
}
|
||||||
|
.joinToString()
|
||||||
|
status = when {
|
||||||
|
document.selectFirst(".fa-flag-alt") != null -> SManga.COMPLETED
|
||||||
|
document.selectFirst(".fa-arrow-right") != null -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
thumbnail_url = document.selectFirst("img.img-thumbnail")?.absUrl("data-src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "ul.ep_ul li.list-group-item"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
element.selectFirst("h3 p")!!.let {
|
||||||
|
val id = it.id().substringAfter("epi_title_")
|
||||||
|
|
||||||
|
url = "/reader?cid=$id"
|
||||||
|
name = it.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
date_upload = try {
|
||||||
|
val date = element.selectFirst("p")!!.ownText()
|
||||||
|
|
||||||
|
dateFormat.parse(date)!!.time
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
return document.select(chapterListSelector())
|
||||||
|
.filter { e ->
|
||||||
|
e.selectFirst("p")?.ownText()?.contains("Arrives on") != true
|
||||||
|
}
|
||||||
|
.map { chapterFromElement(it) }
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val reader by lazy { SpeedBinbReader(client, headers, json) }
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
if (document.selectFirst("a[href\$=account/sign-up]") != null) {
|
||||||
|
throw Exception("Sign up in WebView to read this chapter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.selectFirst("a:contains(UNLOCK NOW)") != null) {
|
||||||
|
throw Exception("Purchase this chapter in WebView")
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.pageListParse(document)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
SortFilter(),
|
||||||
|
AccessTypeFilter(),
|
||||||
|
ReleaseStatusFilter(),
|
||||||
|
LetterFilter(),
|
||||||
|
CategoryFilter(),
|
||||||
|
SpicyLevelFilter(),
|
||||||
|
FormatFilter(),
|
||||||
|
RatingFilter(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
|
Loading…
x
Reference in New Issue
Block a user