Add mangabtt (#398)

* add mangabtt

* replace tab with spaces

* minor cleanup
This commit is contained in:
Secozzi 2024-01-20 10:58:23 +00:00 committed by Draff
parent a14c354a5b
commit c3f277e342
9 changed files with 349 additions and 0 deletions

View File

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

View File

@ -0,0 +1,8 @@
ext {
extName = 'MangaBTT'
extClass = '.MangaBTT'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,263 @@
package eu.kanade.tachiyomi.extension.en.mangabtt
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar
class MangaBTT : ParsedHttpSource() {
override val name = "MangaBTT"
override val baseUrl = "https://mangabtt.com"
override val lang = "en"
override val supportsLatest = true
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
}
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortByFilter(default = 2),
StatusFilter(default = 1),
GenreFilter(default = 1),
),
)
override fun popularMangaSelector(): String =
searchMangaSelector()
override fun popularMangaFromElement(element: Element): SManga =
searchMangaFromElement(element)
override fun popularMangaNextPageSelector(): String =
searchMangaNextPageSelector()
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortByFilter(default = 8),
StatusFilter(default = 1),
GenreFilter(default = 1),
),
)
override fun latestUpdatesSelector(): String =
searchMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga =
searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String =
searchMangaNextPageSelector()
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/find-story".toHttpUrl().newBuilder().apply {
if (query.isNotBlank()) {
addQueryParameter("keyword", query)
} else {
val genre = filters.firstInstanceOrNull<GenreFilter>()?.selectedValue.orEmpty()
val status = filters.firstInstanceOrNull<StatusFilter>()?.selectedValue.orEmpty()
val sortBy = filters.firstInstanceOrNull<SortByFilter>()?.selectedValue.orEmpty()
addQueryParameter("status", status)
addQueryParameter("sort", sortBy)
if (genre.isNotBlank()) {
addPathSegment(genre)
}
}
addQueryParameter("page", page.toString())
}
return GET(url.build(), headers)
}
override fun searchMangaSelector(): String = ".items > .row > .item"
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst(".image img")?.imgAttr()
element.selectFirst("figcaption h3 a")!!.run {
title = text()
setUrlWithoutDomain(attr("abs:href"))
}
}
override fun searchMangaNextPageSelector(): String =
"ul.pagination > li.active + li:not(.disabled)"
// =============================== Filters ==============================
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Ignored when using text search"),
Filter.Separator(),
GenreFilter(),
StatusFilter(),
SortByFilter(),
)
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.selectFirst("h1.title-detail")!!.text()
description = document.selectFirst(".detail-content p")?.text()
?.substringAfter("comic site. The Summary is ")
document.selectFirst(".detail-info")?.run {
thumbnail_url = selectFirst("img")?.imgAttr()
status = selectFirst(".status p:not(.name)").parseStatus()
genre = select(".kind a").joinToString(", ") { it.text() }
author = selectFirst(".author p:not(.name)")?.text()?.takeUnless {
it.equals("updating", true)
}
}
}
private fun Element?.parseStatus(): Int = with(this?.text()) {
return when {
equals("ongoing", true) -> SManga.ONGOING
equals("Đang cập nhật", true) -> SManga.ONGOING
equals("completed", true) -> SManga.COMPLETED
equals("on-hold", true) -> SManga.ON_HIATUS
equals("canceled", true) -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ==============================
override fun chapterListRequest(manga: SManga): Request {
val postHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", baseUrl.toHttpUrl().host)
add("Origin", baseUrl)
set("Referer", baseUrl + manga)
add("X-Requested-With", "XMLHttpRequest")
}.build()
val postBody = FormBody.Builder()
.add("StoryID", manga.url.substringAfterLast("-"))
.build()
return POST("$baseUrl/Story/ListChapterByStoryID", postHeaders, postBody)
}
override fun chapterListSelector() = "ul > li:not(.heading)"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
element.selectFirst(".col-xs-4")?.also {
date_upload = it.text().parseRelativeDate()
}
element.selectFirst("a")!!.run {
name = text()
setUrlWithoutDomain(attr("abs:href"))
}
}
// From OppaiStream
private fun String.parseRelativeDate(): Long {
val now = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
var parsedDate = 0L
val relativeDate = this.split(" ").firstOrNull()
?.replace("one", "1")
?.replace("a", "1")
?.toIntOrNull()
?: return 0L
when {
// parse: 30 seconds ago
"second" in this -> {
parsedDate = now.apply { add(Calendar.SECOND, -relativeDate) }.timeInMillis
}
// parses: "42 minutes ago"
"minute" in this -> {
parsedDate = now.apply { add(Calendar.MINUTE, -relativeDate) }.timeInMillis
}
// parses: "1 hour ago" and "2 hours ago"
"hour" in this -> {
parsedDate = now.apply { add(Calendar.HOUR, -relativeDate) }.timeInMillis
}
// parses: "2 days ago"
"day" in this -> {
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -relativeDate) }.timeInMillis
}
// parses: "2 weeks ago"
"week" in this -> {
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -relativeDate) }.timeInMillis
}
// parses: "2 months ago"
"month" in this -> {
parsedDate = now.apply { add(Calendar.MONTH, -relativeDate) }.timeInMillis
}
// parse: "2 years ago"
"year" in this -> {
parsedDate = now.apply { add(Calendar.YEAR, -relativeDate) }.timeInMillis
}
}
return parsedDate
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select(".reading-detail > .page-chapter").map { page ->
val img = page.selectFirst("img[data-index]")!!
val index = img.attr("data-index").toInt()
val url = img.imgAttr()
Page(index, imageUrl = 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 ==============================
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")
}
}

View File

@ -0,0 +1,76 @@
package eu.kanade.tachiyomi.extension.en.mangabtt
import eu.kanade.tachiyomi.source.model.Filter
data class FilterOption(val displayName: String, val value: String)
inline fun <reified T> List<*>.firstInstanceOrNull() = firstOrNull { it is T } as? T
open class EnhancedSelect(name: String, private val _values: List<FilterOption>, state: Int = 0) :
Filter.Select<String>(name, _values.map { it.displayName }.toTypedArray(), state) {
val selectedValue: String?
get() = _values.getOrNull(state)?.value
}
class SortByFilter(default: Int = 1) : EnhancedSelect(
"Sort By",
listOf(
FilterOption("Top day", "13"),
FilterOption("Top week", "12"),
FilterOption("Top month", "11"),
FilterOption("Top All", "10"),
FilterOption("Comment", "25"),
FilterOption("New Manga", "15"),
FilterOption("Chapter", "30"),
FilterOption("Latest Updates", "0"),
),
default - 1,
)
class StatusFilter(default: Int = 1) : EnhancedSelect(
"Status",
listOf(
FilterOption("All", "-1"),
FilterOption("Completed", "2"),
FilterOption("Ongoing", "1"),
),
default - 1,
)
class GenreFilter(default: Int = 1) : EnhancedSelect(
"Genre",
listOf(
FilterOption("All", ""),
FilterOption("Action", "action"),
FilterOption("ADVENTURE", "adventure"),
FilterOption("Comedy", "comedy"),
FilterOption("Cooking", "cooking"),
FilterOption("Drama", "drama"),
FilterOption("Fantasy", "fantasy"),
FilterOption("Historical", "historical"),
FilterOption("Horror", "horror"),
FilterOption("Isekai", "isekai"),
FilterOption("Josei", "josei"),
FilterOption("Manhua", "manhua"),
FilterOption("Manhwa", "manhwa"),
FilterOption("Martial Arts", "martial-arts"),
FilterOption("Mecha", "mecha"),
FilterOption("MYSTERY", "mystery"),
FilterOption("PSYCHOLOGICAL", "psychological"),
FilterOption("Romance", "romance"),
FilterOption("School Life", "school-life"),
FilterOption("Sci fi", "sci-fi"),
FilterOption("Seinen", "seinen"),
FilterOption("Shoujo", "shoujo"),
FilterOption("Shounen", "shounen"),
FilterOption("SLICE OF LIF", "slice-of-lif"),
FilterOption("Slice of Life", "slice-of-life"),
FilterOption("Sports", "sports"),
FilterOption("SUGGESTIVE", "suggestive"),
FilterOption("SUPERNATURAL", "supernatural"),
FilterOption("TRAGEDY", "tragedy"),
FilterOption("Webtoons", "webtoons"),
),
default - 1,
)