Add mangabtt (#398)
* add mangabtt * replace tab with spaces * minor cleanup
This commit is contained in:
		
							parent
							
								
									a14c354a5b
								
							
						
					
					
						commit
						c3f277e342
					
				
							
								
								
									
										2
									
								
								src/en/mangabtt/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/en/mangabtt/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest />
 | 
			
		||||
							
								
								
									
										8
									
								
								src/en/mangabtt/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/en/mangabtt/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'MangaBTT'
 | 
			
		||||
    extClass = '.MangaBTT'
 | 
			
		||||
    extVersionCode = 1
 | 
			
		||||
    isNsfw = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 9.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/mangabtt/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user