[BlogTruyen] Source is fully back (#16646)

This commit is contained in:
beerpsi 2023-06-06 16:20:34 +07:00 committed by GitHub
parent bdb8f96eed
commit dfb5df3bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 198 additions and 87 deletions

View File

@ -15,6 +15,9 @@
<data android:host="m.blogtruyen.vn" /> <data android:host="m.blogtruyen.vn" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:pathPattern="/tac-gia/..*" />
<data android:pathPattern="/nhom-dich/..*" />
<!-- <!--
Try to ensure that the passed URL is a chapter `c{id}` or a manga `{id}`, with `id` Try to ensure that the passed URL is a chapter `c{id}` or a manga `{id}`, with `id`
being a number. being a number.

View File

@ -5,7 +5,7 @@ ext {
extName = 'BlogTruyen' extName = 'BlogTruyen'
pkgNameSuffix = 'vi.blogtruyen' pkgNameSuffix = 'vi.blogtruyen'
extClass = '.BlogTruyen' extClass = '.BlogTruyen'
extVersionCode = 15 extVersionCode = 16
isNsfw = true isNsfw = true
} }

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.extension.vi.blogtruyen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -17,6 +17,7 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -35,7 +36,7 @@ class BlogTruyen : ParsedHttpSource() {
override val lang = "vi" override val lang = "vi"
override val supportsLatest = false override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1) .rateLimit(1)
@ -45,60 +46,59 @@ class BlogTruyen : ParsedHttpSource() {
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.ENGLISH) private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.ENGLISH)
private lateinit var directory: List<Element> companion object {
const val PREFIX_ID_SEARCH = "id:"
const val PREFIX_AUTHOR_SEARCH = "author:"
const val PREFIX_TEAM_SEARCH = "team:"
}
override fun headersBuilder(): Headers.Builder = override fun headersBuilder(): Headers.Builder =
super.headersBuilder().add("Referer", "$baseUrl/") super.headersBuilder().add("Referer", "$baseUrl/")
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return if (page == 1) {
client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
popularMangaParse(response)
}
} else {
Observable.just(parseDirectory(page))
}
}
override fun popularMangaRequest(page: Int): Request = override fun popularMangaRequest(page: Int): Request =
GET("https://forum.blogtruyen.vn/anh-em-doc-tam/anh-em-doc-tam-75733") GET("$baseUrl/ajax/Search/AjaxLoadListManga?key=tatca&orderBy=3&p=$page", headers)
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
directory = document.select(popularMangaSelector())
return parseDirectory(1)
}
private fun parseDirectory(page: Int): MangasPage { val manga = document.select(popularMangaSelector()).map {
val mangas = mutableListOf<SManga>() val tiptip = it.attr("data-tiptip")
val endRange = ((page * 24) - 1).let { if (it <= directory.lastIndex) it else directory.lastIndex } popularMangaFromElement(it, document.getElementById(tiptip)!!)
for (i in (((page - 1) * 24)..endRange)) {
mangas.add(popularMangaFromElement(directory[i]))
} }
return MangasPage(mangas, endRange < directory.lastIndex) val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null
return MangasPage(manga, hasNextPage)
} }
override fun popularMangaSelector() = "div.topic-content div a" override fun popularMangaSelector() = ".list .tiptip"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { override fun popularMangaFromElement(element: Element): SManga =
setUrlWithoutDomain(element.attr("href")) throw UnsupportedOperationException("Not used")
title = element.text()
thumbnail_url = element.selectFirst("img")?.attr("src") private fun popularMangaFromElement(element: Element, tiptip: Element) = SManga.create().apply {
val anchor = element.selectFirst("a")!!
setUrlWithoutDomain(anchor.attr("href"))
title = anchor.attr("title").replace("truyện tranh ", "").trim()
thumbnail_url = tiptip.selectFirst("img")!!.attr("abs:src")
description = tiptip.selectFirst(".al-j")!!.text()
} }
override fun popularMangaNextPageSelector(): String? = null override fun popularMangaNextPageSelector() = ".paging:last-child:not(.current_page)"
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Unused") override fun latestUpdatesRequest(page: Int): Request =
GET(baseUrl + if (page != 1) "/page-$page" else "", headers)
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Unused") override fun latestUpdatesSelector() = ".storyitem .fl-l"
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Unused") override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
title = element.select("a").attr("title")
thumbnail_url = element.select("img").attr("abs:src")
}
override fun latestUpdatesNextPageSelector(): String? = null override fun latestUpdatesNextPageSelector() = "select.slcPaging option:last-child:not([selected])"
override fun fetchSearchManga( override fun fetchSearchManga(
page: Int, page: Int,
@ -112,8 +112,6 @@ class BlogTruyen : ParsedHttpSource() {
// it's a chapter, resolve to manga ID // it's a chapter, resolve to manga ID
if (id.startsWith("c")) { if (id.startsWith("c")) {
val document = client.newCall(GET("$baseUrl/$id", headers)).execute().asJsoup() val document = client.newCall(GET("$baseUrl/$id", headers)).execute().asJsoup()
throwIfUnapprovedManga(document)
id = document.selectFirst(".breadcrumbs a:last-child")!!.attr("href").removePrefix("/") id = document.selectFirst(".breadcrumbs a:last-child")!!.attr("href").removePrefix("/")
} }
@ -124,34 +122,96 @@ class BlogTruyen : ParsedHttpSource() {
) )
.map { MangasPage(listOf(it.apply { url = "/$id" }), false) } .map { MangasPage(listOf(it.apply { url = "/$id" }), false) }
} }
page == 1 -> client.newCall(searchMangaRequest(page, query, filters)) else -> super.fetchSearchManga(page, query, filters)
.asObservableSuccess() }
.map { response -> }
searchMangaParse(response, query, filters)
private fun extractIdFromQuery(prefix: String, query: String): String {
val q = query.substringAfter(prefix).trim()
return if (q.contains("-")) {
q.substringAfterLast("-")
} else {
q
}
}
private val ajaxSearchUrls: Map<String, String> = mapOf(
PREFIX_AUTHOR_SEARCH to "Author/AjaxLoadMangaByAuthor?orderBy=3",
PREFIX_TEAM_SEARCH to "TranslateTeam/AjaxLoadMangaByTranslateTeam",
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
ajaxSearchUrls.keys.forEach {
if (!query.startsWith(it)) {
return@forEach
}
val id = extractIdFromQuery(it, query)
val url = "$baseUrl/ajax/${ajaxSearchUrls[it]}".toHttpUrl().newBuilder()
.addQueryParameter("id", id)
.addQueryParameter("p", page.toString())
.build()
.toString()
return GET(url, headers)
}
val url = "$baseUrl/timkiem/nangcao/1".toHttpUrl().newBuilder().apply {
addQueryParameter("txt", query)
addQueryParameter("p", page.toString())
val genres = mutableListOf<Int>()
val genresEx = mutableListOf<Int>()
var status = 0
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is GenreList -> filter.state.forEach {
when (it.state) {
Filter.TriState.STATE_INCLUDE -> genres.add(it.id)
Filter.TriState.STATE_EXCLUDE -> genresEx.add(it.id)
else -> {}
}
}
is Author -> {
addQueryParameter("aut", filter.state)
}
is Scanlator -> {
addQueryParameter("gr", filter.state)
}
is Status -> {
status = filter.state
}
else -> {}
} }
else -> Observable.just(parseDirectory(page)) }
}
addPathSegment(status.toString())
addPathSegment(genres.joinToString(","))
addPathSegment(genresEx.joinToString(","))
}.build().toString()
return GET(url, headers)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = override fun searchMangaParse(response: Response): MangasPage {
popularMangaRequest(1) val document = response.asJsoup()
private fun searchMangaParse(response: Response, query: String, filters: FilterList): MangasPage { val manga = document.select(searchMangaSelector()).map {
val trimmedQuery = query.trim() val tiptip = it.attr("data-tiptip")
searchMangaFromElement(it, document.getElementById(tiptip)!!)
directory = response.asJsoup().select(popularMangaSelector()).filter { it ->
it.text().contains(trimmedQuery, ignoreCase = true)
} }
return parseDirectory(1) val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(manga, hasNextPage)
} }
override fun searchMangaSelector(): String = throw UnsupportedOperationException("Unused") override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = override fun searchMangaFromElement(element: Element): SManga =
throw UnsupportedOperationException("Unused") throw UnsupportedOperationException("Not used")
override fun searchMangaNextPageSelector(): String? = null private fun searchMangaFromElement(element: Element, tiptip: Element) =
popularMangaFromElement(element, tiptip)
override fun searchMangaNextPageSelector() = ".pagination .glyphicon-step-forward"
private fun getMangaTitle(document: Document) = document.selectFirst(".entry-title a")!! private fun getMangaTitle(document: Document) = document.selectFirst(".entry-title a")!!
.attr("title") .attr("title")
@ -159,8 +219,6 @@ class BlogTruyen : ParsedHttpSource() {
.trim() .trim()
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
throwIfUnapprovedManga(document)
val anchor = document.selectFirst(".entry-title a")!! val anchor = document.selectFirst(".entry-title a")!!
setUrlWithoutDomain(anchor.attr("href")) setUrlWithoutDomain(anchor.attr("href"))
title = getMangaTitle(document) title = getMangaTitle(document)
@ -235,9 +293,6 @@ class BlogTruyen : ParsedHttpSource() {
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
throwIfUnapprovedManga(document)
val title = getMangaTitle(document) val title = getMangaTitle(document)
return document.select(chapterListSelector()).map { chapterFromElement(it, title) } return document.select(chapterListSelector()).map { chapterFromElement(it, title) }
} }
@ -276,8 +331,6 @@ class BlogTruyen : ParsedHttpSource() {
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
throwIfUnapprovedManga(document)
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
document.select("#content > img").forEachIndexed { i, e -> document.select("#content > img").forEachIndexed { i, e ->
@ -301,30 +354,79 @@ class BlogTruyen : ParsedHttpSource() {
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used") override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
private fun isPageUnblocked(document: Document): Boolean { private class Status : Filter.Select<String>(
val anchor = document.selectFirst("center b a")?.attr("href") ?: return true "Status",
val img = document.selectFirst("center img")?.attr("src") ?: return true arrayOf("Sao cũng được", "Đang tiến hành", "Đã hoàn thành", "Tạm ngưng"),
)
val content = document.body().text() private class Author : Filter.Text("Tác giả")
private class Scanlator : Filter.Text("Nhóm dịch")
private class Genre(name: String, val id: Int) : Filter.TriState(name)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Thể loại", genres)
if (content != UNDERGOING_CHECK || anchor != APPROVED_MANGA_POST || img != DONATION_IMAGE) { override fun getFilterList() = FilterList(
return true Author(),
} Scanlator(),
Status(),
GenreList(getGenreList()),
)
return false private fun getGenreList() = listOf(
} Genre("16+", 54),
Genre("18+", 45),
private fun throwIfUnapprovedManga(document: Document) { Genre("Action", 1),
if (!isPageUnblocked(document)) { Genre("Adult", 2),
throw Exception("Truyện chưa được kiểm duyệt!") Genre("Adventure", 3),
} Genre("Anime", 4),
} Genre("Comedy", 5),
Genre("Comic", 6),
companion object { Genre("Doujinshi", 7),
const val PREFIX_ID_SEARCH = "id:" Genre("Drama", 49),
Genre("Ecchi", 48),
const val UNDERGOING_CHECK = "Website đang rà soát lại nội dung. Anh em đọc tạm một số truyện nhẹ nhàng lành mạnh đã được kiểm duyệt TẠI ĐÂY Website đang trong giai đoạn khó khăn, nhưng hãy donate chỉ khi bạn sẵn lòng và vui vẻ thôi nhé \uD83D\uDC96" Genre("Even BT", 60),
const val APPROVED_MANGA_POST = "https://forum.blogtruyen.vn/anh-em-doc-tam/anh-em-doc-tam-75733" Genre("Fantasy", 50),
const val DONATION_IMAGE = "https://blogtruyen.vn/xin-donate.png" Genre("Game", 61),
} Genre("Gender Bender", 51),
Genre("Harem", 12),
Genre("Historical", 13),
Genre("Horror", 14),
Genre("Isekai/Dị Giới", 63),
Genre("Josei", 15),
Genre("Live Action", 16),
Genre("Magic", 46),
Genre("Manga", 55),
Genre("Manhua", 17),
Genre("Manhwa", 18),
Genre("Martial Arts", 19),
Genre("Mature", 20),
Genre("Mecha", 21),
Genre("Mystery", 22),
Genre("Nấu ăn", 56),
Genre("NTR", 62),
Genre("One shot", 23),
Genre("Psychological", 24),
Genre("Romance", 25),
Genre("School Life", 26),
Genre("Sci-fi", 27),
Genre("Seinen", 28),
Genre("Shoujo", 29),
Genre("Shoujo Ai", 30),
Genre("Shounen", 31),
Genre("Shounen Ai", 32),
Genre("Slice of Life", 33),
Genre("Smut", 34),
Genre("Soft Yaoi", 35),
Genre("Soft Yuri", 36),
Genre("Sports", 37),
Genre("Supernatural", 38),
Genre("Tạp chí truyện tranh", 39),
Genre("Tragedy", 40),
Genre("Trap", 58),
Genre("Trinh thám", 57),
Genre("Truyện scan", 41),
Genre("Video clip", 53),
Genre("VnComic", 42),
Genre("Webtoon", 52),
Genre("Yuri", 59),
)
} }

View File

@ -16,7 +16,13 @@ class BlogTruyenUrlActivity : Activity() {
startActivity( startActivity(
Intent().apply { Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH" action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${BlogTruyen.PREFIX_ID_SEARCH}${pathSegments[0]}/${pathSegments[1]}") with(pathSegments[0]) {
when {
equals("tac-gia") -> putExtra("query", "${BlogTruyen.PREFIX_AUTHOR_SEARCH}${pathSegments[1]}")
equals("nhom-dich") -> putExtra("query", "${BlogTruyen.PREFIX_TEAM_SEARCH}${pathSegments[1]}")
else -> putExtra("query", "${BlogTruyen.PREFIX_ID_SEARCH}${pathSegments[0]}/${pathSegments[1]}")
}
}
putExtra("filter", packageName) putExtra("filter", packageName)
}, },
) )