Add manga sect (#19403)
* Add manga sect * null stuff, change ratelimit
This commit is contained in:
parent
030f1f8891
commit
9597bd45de
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Manga Sect'
|
||||
pkgNameSuffix = 'en.mangasect'
|
||||
extClass = '.MangaSect'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
|
@ -0,0 +1,238 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangasect
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaSect : ParsedHttpSource() {
|
||||
|
||||
override val name = "Manga Sect"
|
||||
|
||||
override val baseUrl = "https://mangasect.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(1)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
|
||||
|
||||
override fun popularMangaSelector(): String = "div#main div.grid > div"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
||||
element.selectFirst(".text-center a")!!.run {
|
||||
title = text().trim()
|
||||
setUrlWithoutDomain(attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/all-manga/$page/?sort=1", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun latestUpdatesSelector(): String =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (query.isNotBlank()) {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("keyword", query)
|
||||
} else {
|
||||
addPathSegment("filter")
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> {
|
||||
if (filter.checked.isNotEmpty()) {
|
||||
addQueryParameter("genres", filter.checked.joinToString(","))
|
||||
}
|
||||
}
|
||||
is StatusFilter -> {
|
||||
if (filter.selected.isNotBlank()) {
|
||||
addQueryParameter("status", filter.selected)
|
||||
}
|
||||
}
|
||||
is SortFilter -> {
|
||||
addQueryParameter("sort", filter.selected)
|
||||
}
|
||||
is ChapterCountFilter -> {
|
||||
addQueryParameter("chapter_count", filter.selected)
|
||||
}
|
||||
is GenderFilter -> {
|
||||
addQueryParameter("sex", filter.selected)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPathSegment(page.toString())
|
||||
addPathSegment("")
|
||||
}
|
||||
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaSelector(): String =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("Ignored when using text search"),
|
||||
Filter.Separator(),
|
||||
GenreFilter(),
|
||||
ChapterCountFilter(),
|
||||
GenderFilter(),
|
||||
StatusFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
description = document.selectFirst("div#syn-target")?.text()
|
||||
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
|
||||
title = document.selectFirst(".a2 header h1")?.text()?.trim() ?: "N/A"
|
||||
genre = document.select(".a2 div > a[rel='tag'].label").joinToString(", ") { it.text() }
|
||||
|
||||
document.selectFirst(".a1 > aside")?.run {
|
||||
author = select("div:contains(Authors) > span a")
|
||||
.joinToString(", ") { it.text().trim() }
|
||||
.takeUnless { it.isBlank() || it.equals("Updating", true) }
|
||||
status = selectFirst("div:contains(Status) > span")?.text().let(::parseStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?): Int = when {
|
||||
status.equals("ongoing", true) -> SManga.ONGOING
|
||||
status.equals("completed", true) -> SManga.COMPLETED
|
||||
status.equals("on-hold", true) -> SManga.ON_HIATUS
|
||||
status.equals("canceled", true) -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector() = "ul > li.chapter"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst("time[datetime]")?.also {
|
||||
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
|
||||
}
|
||||
element.selectFirst("a")!!.run {
|
||||
text().trim().also {
|
||||
name = it
|
||||
chapter_number = it.substringAfter("hapter ").toFloatOrNull() ?: 0F
|
||||
}
|
||||
setUrlWithoutDomain(attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val pageHeaders = headersBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
add("Referer", baseUrl + chapter.url)
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
val id = chapter.url.split("/").last()
|
||||
return GET("$baseUrl/ajax/image/list/chap/$id", pageHeaders)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PageListResponseDto(val html: String)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val data = response.parseAs<PageListResponseDto>().html
|
||||
return pageListParse(
|
||||
Jsoup.parseBodyFragment(
|
||||
data,
|
||||
response.request.header("Referer")!!,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div.separator").map { page ->
|
||||
val index = page.attr("data-index").toInt()
|
||||
val url = page.selectFirst("a")!!.attr("abs:href")
|
||||
Page(index, document.location(), url)
|
||||
}.sortedBy { it.index }
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeaders = headersBuilder().apply {
|
||||
add("Accept", "image/avif,image/webp,*/*")
|
||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||
}.build()
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
// From mangathemesia
|
||||
private fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangasect
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
val selected get() = options[state].second
|
||||
}
|
||||
|
||||
class CheckBoxFilter(
|
||||
name: String,
|
||||
val value: String,
|
||||
) : Filter.CheckBox(name)
|
||||
|
||||
class ChapterCountFilter : SelectFilter("Chapter count", chapterCount) {
|
||||
companion object {
|
||||
private val chapterCount = listOf(
|
||||
Pair(">= 0", "0"),
|
||||
Pair(">= 10", "10"),
|
||||
Pair(">= 30", "30"),
|
||||
Pair(">= 50", "50"),
|
||||
Pair(">= 100", "100"),
|
||||
Pair(">= 200", "200"),
|
||||
Pair(">= 300", "300"),
|
||||
Pair(">= 400", "400"),
|
||||
Pair(">= 500", "500"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GenderFilter : SelectFilter("Manga Gender", gender) {
|
||||
companion object {
|
||||
private val gender = listOf(
|
||||
Pair("All", "All"),
|
||||
Pair("Boy", "Boy"),
|
||||
Pair("Girl", "Girl"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StatusFilter : SelectFilter("Status", status) {
|
||||
companion object {
|
||||
private val status = listOf(
|
||||
Pair("All", ""),
|
||||
Pair("Completed", "completed"),
|
||||
Pair("OnGoing", "on-going"),
|
||||
Pair("On-Hold", "on-hold"),
|
||||
Pair("Canceled", "canceled"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : SelectFilter("Sort", sort) {
|
||||
companion object {
|
||||
private val sort = listOf(
|
||||
Pair("Default", "default"),
|
||||
Pair("Latest Updated", "latest-updated"),
|
||||
Pair("Most Viewed", "most-viewd"),
|
||||
Pair("Score", "score"),
|
||||
Pair("Name A-Z", "az"),
|
||||
Pair("Name Z-A", "za"),
|
||||
Pair("Newest", "new"),
|
||||
Pair("Oldest", "old"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GenreFilter : Filter.Group<CheckBoxFilter>(
|
||||
"Genre",
|
||||
genres.map { CheckBoxFilter(it.first, it.second) },
|
||||
) {
|
||||
val checked get() = state.filter { it.state }.map { it.value }
|
||||
|
||||
companion object {
|
||||
private val genres = listOf(
|
||||
Pair("Action", "29"),
|
||||
Pair("Adaptation", "66"),
|
||||
Pair("Adult", "108"),
|
||||
Pair("Adventure", "33"),
|
||||
Pair("Aliens", "2326"),
|
||||
Pair("Animals", "199"),
|
||||
Pair("Comedy", "35"),
|
||||
Pair("Comic", "109"),
|
||||
Pair("Cooking", "26"),
|
||||
Pair("Crime", "274"),
|
||||
Pair("Delinquents", "234"),
|
||||
Pair("Demons", "136"),
|
||||
Pair("Drama", "39"),
|
||||
Pair("Dungeons", "204"),
|
||||
Pair("Ecchi", "54"),
|
||||
Pair("Fantasy", "30"),
|
||||
Pair("Full Color", "27"),
|
||||
Pair("Genderswap", "1441"),
|
||||
Pair("Genius MC", "209"),
|
||||
Pair("Ghosts", "1527"),
|
||||
Pair("Gore", "1678"),
|
||||
Pair("Harem", "43"),
|
||||
Pair("Historical", "49"),
|
||||
Pair("Horror", "69"),
|
||||
Pair("Incest", "1189"),
|
||||
Pair("Isekai", "40"),
|
||||
Pair("Loli", "198"),
|
||||
Pair("Long Strip", "233"),
|
||||
Pair("Magic", "212"),
|
||||
Pair("Magical Girls", "1676"),
|
||||
Pair("Manhua", "58"),
|
||||
Pair("Manhwa", "80"),
|
||||
Pair("Martial Arts", "32"),
|
||||
Pair("Mature", "34"),
|
||||
Pair("Mecha", "70"),
|
||||
Pair("Medical", "2113"),
|
||||
Pair("Military", "1531"),
|
||||
Pair("Monster", "218"),
|
||||
Pair("Monster Girls", "201"),
|
||||
Pair("Monsters", "63"),
|
||||
Pair("Murim", "208"),
|
||||
Pair("Music", "412"),
|
||||
Pair("Mystery", "31"),
|
||||
Pair("One shot", "155"),
|
||||
Pair("Overpowered", "206"),
|
||||
Pair("Police", "275"),
|
||||
Pair("Post-Apocalyptic", "197"),
|
||||
Pair("Psychological", "36"),
|
||||
Pair("Rebirth", "1435"),
|
||||
Pair("Recarnation", "67"),
|
||||
Pair("Regression", "205"),
|
||||
Pair("Reincarnation", "64"),
|
||||
Pair("Return", "1454"),
|
||||
Pair("Returner", "211"),
|
||||
Pair("Revenge", "219"),
|
||||
Pair("Romance", "37"),
|
||||
Pair("School Life", "44"),
|
||||
Pair("Sci fi", "42"),
|
||||
Pair("Sci-fi", "216"),
|
||||
Pair("Seinen", "52"),
|
||||
Pair("Sexual Violence", "2325"),
|
||||
Pair("Shota", "2327"),
|
||||
Pair("Shoujo", "92"),
|
||||
Pair("Shounen", "38"),
|
||||
Pair("Shounen ai", "103"),
|
||||
Pair("Slice of Life", "68"),
|
||||
Pair("Super power", "213"),
|
||||
Pair("Superhero", "1630"),
|
||||
Pair("Supernatural", "41"),
|
||||
Pair("Survival", "463"),
|
||||
Pair("System", "203"),
|
||||
Pair("Thriller", "462"),
|
||||
Pair("Time travel", "65"),
|
||||
Pair("tower", "207"),
|
||||
Pair("Tragedy", "51"),
|
||||
Pair("Transmigration", "217"),
|
||||
Pair("Uncategorized", "55"),
|
||||
Pair("Vampires", "200"),
|
||||
Pair("Video Games", "1606"),
|
||||
Pair("Virtual Reality", "757"),
|
||||
Pair("Web comic", "98"),
|
||||
Pair("Webtoons", "77"),
|
||||
Pair("Wuxia", "202"),
|
||||
Pair("Zombies", "464"),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue