Add Manga Planet (#1319)

This commit is contained in:
beerpsi 2024-02-17 15:55:11 +07:00 committed by GitHub
parent 1ba5dd036c
commit b1da5a83b6
9 changed files with 385 additions and 0 deletions

View 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"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)