Add mangabtt (#398)
* add mangabtt * replace tab with spaces * minor cleanup
This commit is contained in:
parent
a14c354a5b
commit
c3f277e342
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -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 |
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
Loading…
Reference in New Issue