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