feat(BoaBua): add source BaoBua (#8253)

* feat(buondua): add source BaoBua

Refs: #1104

* fix(buondua/search): resolve pagination param forwarding
chore(buondua/search): clean up URL format and flag

* Fix:
  - Add missing pagination parameter propagation
* Maintenance:
  - Remove redundant trailing "/" in pagination URLs
  - Set `supportsLatest` to false (default behavior)

Closes: #1104

* chore(buondua): remove redundant trim() calls

Closes: #1104

* refactor(BaoBua): standardize chapter names and URL handling

- Replace date-based chapter names with static "Gallery" value
- Remove baseUrl from category URLs (construct dynamically when used)

Closes: #1104

* chore(BaoBua): remove unused

Closes: #1104

* chore(BaoBua): revert settings.gradle.kts

Closes: #1104

* chore(BaoBua): remove unused import

Closes: #1104

* chore(BaoBua): remove needless blank line

Closes: #1104

* fix(BaoBua): add unselected Category

Closes: #1104

* refactor(BaoBua): optimize manga details parsing

- Set update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
- Remove unused randomua dependency

Closes: #1104
This commit is contained in:
marioplus 2025-03-31 23:07:53 +08:00 committed by Draff
parent 40a9d2ec6a
commit fad76bc4b2
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
9 changed files with 211 additions and 0 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.network.GET
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.model.UpdateStrategy
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstance
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class BaoBua() : SimpleParsedHttpSource() {
override val baseUrl = "https://www.baobua.net"
override val lang = "all"
override val name = "BaoBua"
override val supportsLatest = false
override fun simpleMangaSelector() = "article.post"
override fun simpleMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a.popunder")!!.absUrl("href"))
title = element.selectFirst("div.read-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
override fun simpleNextPageSelector(): String = "nav.pagination a.next"
// region popular
override fun popularMangaRequest(page: Int) = GET("$baseUrl?page=$page", headers)
// endregion
// region latest
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
// endregion
// region Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filter = filters.firstInstance<SourceCategorySelector>()
return filter.selectedCategory?.let {
GET(it.buildUrl(baseUrl), headers)
} ?: run {
baseUrl.toHttpUrl().newBuilder()
.addEncodedQueryParameter("q", query)
.addEncodedQueryParameter("page", page.toString())
.build()
.let { GET(it, headers) }
}
}
// region Details
override fun mangaDetailsParse(document: Document): SManga {
val trailItemsEl = document.selectFirst("div.breadcrumb-trail > ul.trail-items")!!
return SManga.create().apply {
title = trailItemsEl.selectFirst("li.trail-end")!!.text()
genre = trailItemsEl.select("li:not(.trail-end):not(.trail-begin)").joinToString { it.text() }
}
}
override fun chapterListSelector() = "html"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
chapter_number = 0F
setUrlWithoutDomain(element.selectFirst("div.breadcrumb-trail li.trail-end > a")!!.absUrl("href"))
date_upload = POST_DATE_FORMAT.tryParse(element.selectFirst("span.item-metadata.posts-date")?.text())
name = "Gallery"
}
// endregion
// region Pages
override fun pageListParse(document: Document): List<Page> {
val basePageUrl = document.selectFirst("div.breadcrumb-trail li.trail-end > a")!!.absUrl("href")
val maxPage: Int = document.selectFirst("div.nav-links > a.next.page-numbers")?.text()?.toInt() ?: 1
var pageIndex = 0
return (1..maxPage).flatMap { pageNum ->
val doc = if (pageNum == 1) {
document
} else {
client.newCall(GET("$basePageUrl?p=$pageNum", headers)).execute().asJsoup()
}
doc.select("div.entry-content.read-details img.wp-image")
.map { Page(pageIndex++, imageUrl = it.absUrl("src")) }
}
}
// endregion
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Unable to further search in the category!"),
Filter.Separator(),
SourceCategorySelector.create(baseUrl),
)
companion object {
private val POST_DATE_FORMAT = SimpleDateFormat("EEE MMM dd yyyy", Locale.US)
}
}

View File

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
abstract class SimpleParsedHttpSource : ParsedHttpSource() {
abstract fun simpleMangaSelector(): String
abstract fun simpleMangaFromElement(element: Element): SManga
abstract fun simpleNextPageSelector(): String?
// region popular
override fun popularMangaSelector() = simpleMangaSelector()
override fun popularMangaNextPageSelector() = simpleNextPageSelector()
override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element)
// endregion
// region last
override fun latestUpdatesSelector() =
if (supportsLatest) simpleMangaSelector() else throw throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) =
if (supportsLatest) simpleMangaFromElement(element) else throw throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() =
if (supportsLatest) simpleNextPageSelector() else throw throw UnsupportedOperationException()
// endregion
// region search
override fun searchMangaSelector() = simpleMangaSelector()
override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element)
override fun searchMangaNextPageSelector() = simpleNextPageSelector()
// endregion
override fun chapterListSelector() = simpleMangaSelector()
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// endregion
}

View File

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl.Companion.toHttpUrl
data class SourceCategory(private val name: String, var cat: String) {
override fun toString() = this.name
fun buildUrl(baseUrl: String): String {
return "$baseUrl/".toHttpUrl().newBuilder()
.addEncodedQueryParameter("cat", this.cat)
.build()
.toString()
}
}
class SourceCategorySelector(
name: String,
categories: List<SourceCategory>,
) : Filter.Select<SourceCategory>(name, categories.toTypedArray()) {
val selectedCategory: SourceCategory?
get() = if (state > 0) values[state] else null
companion object {
fun create(baseUrl: String): SourceCategorySelector {
val options = listOf(
SourceCategory("unselected", ""),
SourceCategory("大胸美女", "YmpydEtkNzV5NHJKcDJYVGtOVW0yZz09"),
SourceCategory("巨乳美女", "Q09EdlMvMHgweERrUitScTFTaDM4Zz09"),
SourceCategory("全裸写真", "eXZzejJPNFRVNzJqKzFDUmNzZEU2QT09"),
SourceCategory("chinese", "bG9LamJsWWdSbGcyY0FEZytldkhTZz09"),
SourceCategory("chinese models", "OCtTSEI2YzRTcWMvWUsyeDM0aHdzdUIwWDlHMERZUEZaVHUwUEVUVWo3QT0"),
SourceCategory("korean", "Tm1ydGlaZ1A2YWM3a3BvYWh6L3dIdz09"),
SourceCategory("korea", "bzRjeWR0akQrRWpxRE1xOGF6TW5Tdz09"),
SourceCategory("korean models", "TGZTVGtwOCtxTW1TQU1KYWhUb01DQT09"),
SourceCategory("big boobs", "UmFLQVkvVndGNlpPckwvZkpVaEE4UT09"),
SourceCategory("adult", "b2RFSnlwdWxyREMxVmRpcThKVXRLUT09"),
SourceCategory("nude-art", "djFqa293VmFZMEJLdDlUWndsMGtldz09"),
SourceCategory("Asian adult photo", "SHBGZHFueTVNeUlxVHRLaU53RjU2NS9VcjNxRVg3VnhqTGJoK25YaVQ1UT0"),
SourceCategory("cosplay", "OEI2c000ZDBxakwydjZIUVJaRnlMQT09"),
SourceCategory("hot", "c3VRb3RJZ2wrU2tTYmpGSUVqMnFndz09"),
SourceCategory("big breast", "dkQ3b0RiK0xpZDRlMVNSY3lUNkJXQT09"),
)
return SourceCategorySelector("Category", options)
}
}
}