Add manga sect (#19403)

* Add manga sect

* null stuff, change ratelimit
This commit is contained in:
Secozzi 2023-12-25 15:23:23 +00:00 committed by GitHub
parent 030f1f8891
commit 9597bd45de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 419 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

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

View File

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

View File

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