mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-25 03:33:24 +01:00
Add Manga Planet (#1319)
This commit is contained in:
parent
1ba5dd036c
commit
b1da5a83b6
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…
Reference in New Issue
Block a user