Doujindesu error 404 HotFix (#11128)

* Sometimes i found another problem

- Fixed Author, Group and Series Filters only 1 active (many intentionally or unintentionally enter input in several Filters which makes the results Null)
- Fixed all HTML Tags such as &bnsp;, > and < and so on in the description
- Fixed descriptions for Manhwa that were cut off due to different logic

* Fix DoujinDesu `404` HOTFIX

* Remove unused pattern

* Remove Log

* Revert Description

Reverted to May Version Description and slightly make better
This commit is contained in:
TheKingTermux 2025-10-19 16:16:18 +07:00 committed by Draff
parent d1bed69ada
commit 7a61751a50
Signed by: Draff
GPG Key ID: E8A89F3211677653
2 changed files with 87 additions and 86 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'DoujinDesu' extName = 'DoujinDesu'
extClass = '.DoujinDesu' extClass = '.DoujinDesu'
extVersionCode = 10 extVersionCode = 11
isNsfw = true isNsfw = true
} }

View File

@ -20,9 +20,9 @@ import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.getPreferencesLazy
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 org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -249,9 +249,19 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
Genre("Yuri"), Genre("Yuri"),
) )
private class AuthorFilter : Filter.Text("Author") private class AuthorGroupSeriesOption(val display: String, val key: String) {
private class GroupFilter : Filter.Text("Group") override fun toString(): String = display
private class SeriesFilter : Filter.Text("Series") }
private val authorGroupSeriesOptions = arrayOf(
AuthorGroupSeriesOption("None", ""),
AuthorGroupSeriesOption("Author", "author"),
AuthorGroupSeriesOption("Group", "group"),
AuthorGroupSeriesOption("Series", "series"),
)
private class AuthorGroupSeriesFilter(options: Array<AuthorGroupSeriesOption>) : Filter.Select<AuthorGroupSeriesOption>("Filter by Author/Group/Series", options, 0)
private class AuthorGroupSeriesValueFilter : Filter.Text("Nama Author/Group/Series")
private class CharacterFilter : Filter.Text("Karakter") private class CharacterFilter : Filter.Text("Karakter")
private class CategoryNames(categories: Array<Category>) : Filter.Select<Category>("Kategori", categories, 0) private class CategoryNames(categories: Array<Category>) : Filter.Select<Category>("Kategori", categories, 0)
private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Urutkan", orders, 0) private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Urutkan", orders, 0)
@ -295,7 +305,8 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
basicInformationFromElement(element) basicInformationFromElement(element)
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/manga/page/$page/?title=&author=&character=&statusx=&typex=&order=popular", headers) // Original url $baseUrl/manga/page/$page/?title=&author=&character=&statusx=&typex=&order=popular
return GET("$baseUrl/manhwa/page/$page/", headers)
} }
// Latest // Latest
@ -304,7 +315,8 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
basicInformationFromElement(element) basicInformationFromElement(element)
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/manga/page/$page/?title=&author=&character=&statusx=&typex=&order=update", headers) // Original url $baseUrl/manga/page/$page/?title=&author=&character=&statusx=&typex=&order=update
return GET("$baseUrl/doujin/page/$page/", headers)
} }
// Element Selectors // Element Selectors
@ -321,9 +333,11 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// Anything else filter handling // Anything else filter handling
val url = "$baseUrl/manga/page/$page/".toHttpUrl().newBuilder() val baseUrlWithPage = if (page == 1) "$baseUrl/" else "$baseUrl/page/$page/"
url.addQueryParameter("title", query.ifBlank { "" })
val finalUrl = if (query.isNotBlank()) "$baseUrlWithPage?s=${query.replace(" ", "+")}" else baseUrlWithPage
/* Will be used later if DoujinDesu aleardy fix their problem
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) { when (filter) {
is CategoryNames -> { is CategoryNames -> {
@ -351,88 +365,61 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
else -> {} else -> {}
} }
} }
*/
val author = filters.firstInstanceOrNull<AuthorFilter>()?.state?.trim() val agsFilter = filters.firstInstanceOrNull<AuthorGroupSeriesFilter>()
val group = filters.firstInstanceOrNull<GroupFilter>()?.state?.trim() val agsValueFilter = filters.firstInstanceOrNull<AuthorGroupSeriesValueFilter>()
val series = filters.firstInstanceOrNull<SeriesFilter>()?.state?.trim() val selectedOption = agsFilter?.values?.getOrNull(agsFilter.state)
val filterValue = agsValueFilter?.state?.trim() ?: ""
// Author filter handling // Author/Group/Series filter handling
if (query.isBlank()) { if (query.isBlank() && selectedOption != null && selectedOption.key.isNotBlank()) {
if (!author.isNullOrBlank()) { val typePath = selectedOption.key
val slug = author.toMultiSlug() val request = if (filterValue.isBlank()) {
if (slug.isNotBlank()) { val url = if (page == 1) {
val authorUrl = if (page == 1) { "$baseUrl/$typePath/"
"$baseUrl/author/$slug/" } else {
} else { "$baseUrl/$typePath/page/$page/"
"$baseUrl/author/$slug/page/$page/"
}
return GET(authorUrl, headers)
} }
} GET(url, headers)
} else {
// Group filter handling val url = if (page == 1) {
if (!group.isNullOrBlank()) { "$baseUrl/$typePath/$filterValue/"
val slug = group.toMultiSlug() } else {
if (slug.isNotBlank()) { "$baseUrl/$typePath/$filterValue/page/$page/"
val groupUrl = if (page == 1) {
"$baseUrl/group/$slug/"
} else {
"$baseUrl/group/$slug/page/$page/"
}
return GET(groupUrl, headers)
}
}
// Series filter handling
if (!series.isNullOrBlank()) {
val slug = series.toMultiSlug()
if (slug.isNotBlank()) {
val seriesUrl = if (page == 1) {
"$baseUrl/series/$slug/"
} else {
"$baseUrl/series/$slug/page/$page/"
}
return GET(seriesUrl, headers)
} }
GET(url, headers)
} }
return request
} }
return GET(url.build(), headers) return GET(finalUrl, headers)
}
private val nonAlphaNumSpaceDashRegex = Regex("[^a-z0-9\\s-]")
private val multiSpaceRegex = Regex("\\s+")
private fun String.toMultiSlug(): String {
return this
.trim()
.lowercase()
.replace(nonAlphaNumSpaceDashRegex, "")
.replace(multiSpaceRegex, "-")
} }
override fun searchMangaFromElement(element: Element): SManga = override fun searchMangaFromElement(element: Element): SManga =
basicInformationFromElement(element) basicInformationFromElement(element)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Filter.Header("NB: Filter bisa digabungkan dengan memakai pencarian teks selain Author, Group dan Series!"), Filter.Header("NB: Fitur Emergency, jadi maklumi aja jika ada bug!"),
Filter.Separator(), Filter.Separator(),
Filter.Header("NB: Gunakan ini untuk filter per Author, Group dan Series saja, tidak bisa digabungkan dengan memakai pencarian teks dan filter lainnya!"), Filter.Header("NB: Tidak bisa digabungkan dengan memakai pencarian teks dan filter lainnya, serta harus memasukkan nama Author, Group dan Series secara lengkap!"),
AuthorFilter(), AuthorGroupSeriesFilter(authorGroupSeriesOptions),
GroupFilter(), AuthorGroupSeriesValueFilter(),
SeriesFilter(),
Filter.Separator(), Filter.Separator(),
/* Will be used later if DoujinDesu aleardy fix their problem
Filter.Header("NB: Untuk Character Filter akan mengambil hasil apapun jika diinput, misal 'alice', maka hasil akan memunculkan semua Karakter yang memiliki nama 'Alice', bisa digabungkan dengan filter lainnya"), Filter.Header("NB: Untuk Character Filter akan mengambil hasil apapun jika diinput, misal 'alice', maka hasil akan memunculkan semua Karakter yang memiliki nama 'Alice', bisa digabungkan dengan filter lainnya"),
CharacterFilter(), CharacterFilter(),
StatusList(statusList), StatusList(statusList),
CategoryNames(categoryNames), CategoryNames(categoryNames),
OrderBy(orderBy), OrderBy(orderBy),
GenreList(genreList()), GenreList(genreList()),
*/
) )
// Detail Parse // Detail Parse
private val chapterListRegex = Regex("""\d+[-]?\d*\..+<br>""", RegexOption.IGNORE_CASE) private val chapterListRegex = Regex("""\d+[-]?\d*\..+<br>""", RegexOption.IGNORE_CASE)
private val htmlTagRegex = Regex("<[^>]*>") private val htmlTagRegex = Regex("<[^>]*>")
private val chapterPrefixRegex = Regex("""^\d+(-\d+)?\.\s*.*""")
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.selectFirst("section.metadata")!! val infoElement = document.selectFirst("section.metadata")!!
@ -483,22 +470,26 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
val paragraphs = element.select("p") val paragraphs = element.select("p")
val firstText = paragraphs.firstOrNull()?.text()?.trim()?.lowercase() val firstText = paragraphs.firstOrNull()?.text()?.trim()?.lowercase()
// CASE 1: Gabungan chapter dalam satu paragraf // Fungsi untuk mendekode semua entitas HTML
val decodeHtmlEntities = { text: String ->
Jsoup.parse(text).text().replace('\u00A0', ' ')
}
// CASE 1: Gabungan chapter dalam satu paragraf (Manga Style)
val mergedChapterElement = element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))").firstOrNull { val mergedChapterElement = element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))").firstOrNull {
chapterListRegex.containsMatchIn(it.html()) chapterListRegex.containsMatchIn(it.html())
} }
if (mergedChapterElement != null) { if (mergedChapterElement != null) {
val chapterList = mergedChapterElement.html() val chapterList = mergedChapterElement.html()
.split("<br>") .split("<br>")
.drop(1) .drop(1)
.map { it.replace(htmlTagRegex, "").trim() } .map { decodeHtmlEntities(it.replace(htmlTagRegex, "").trim()) }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
return@let "Daftar Chapter:\n" + chapterList.joinToString(" | ") return@let "Daftar Chapter:\n" + chapterList.joinToString(" | ")
} }
// CASE 2: Dua paragraf: p[0] = "Sinopsis:", p[1] = daftar chapter // CASE 2: Dua paragraf: p[0] = "Sinopsis:", p[1] = daftar chapter (Manga Style)
if ( if (
firstText == "sinopsis:" && firstText == "sinopsis:" &&
paragraphs.size > 1 && paragraphs.size > 1 &&
@ -506,39 +497,45 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
) { ) {
val chapterList = paragraphs[1].html() val chapterList = paragraphs[1].html()
.split("<br>") .split("<br>")
.map { it.replace(htmlTagRegex, "").trim() } .map { decodeHtmlEntities(it.replace(htmlTagRegex, "").trim()) }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
return@let "Daftar Chapter:\n" + chapterList.joinToString(" | ") return@let "Daftar Chapter:\n" + chapterList.joinToString(" | ")
} }
// CASE 3: Sinopsis biasa pakai <strong>Sinopsis:</strong> di p awal // CASE 3 + 5 Hybrid: Tangani Sinopsis dengan <strong> + <br> + <p> campuran (Manhwa Style + Terkompresi)
val sinopsisPara = element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))") val sinopsisPara = element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))")
if (sinopsisPara.isNotEmpty()) { if (sinopsisPara.isNotEmpty()) {
val sinopsisStart = sinopsisPara.first()!! val sinopsisStart = sinopsisPara.first()!!
val htmlSplit = sinopsisStart.html().split("<br>") val htmlSplit = sinopsisStart.html().split("<br>")
val startText = htmlSplit.getOrNull(1)?.replace(htmlTagRegex, "")?.trim().orEmpty() val startText = htmlSplit
.drop(1)
.map { decodeHtmlEntities(it.replace(htmlTagRegex, "").trim()) }
.filter { it.isNotEmpty() && !it.lowercase().startsWith("download") && !it.lowercase().startsWith("volume") && !it.lowercase().startsWith("chapter") }
val sinopsisTexts = buildList { val sinopsisTexts = buildList {
if (startText.isNotEmpty()) add(startText) addAll(startText)
val allP = element.select("p") val allP = element.select("p")
val startIndex = allP.indexOf(sinopsisStart) val startIndex = allP.indexOf(sinopsisStart)
for (i in startIndex + 1 until allP.size) { for (i in startIndex + 1 until allP.size) {
val content = allP[i].text().trim() val htmlSplitNext = allP[i].html().split("<br>")
if (!content.lowercase().startsWith("download")) { val contents = htmlSplitNext
add(content) .map { decodeHtmlEntities(it.replace(htmlTagRegex, "").trim()) }
} else { .filter { it.isNotEmpty() && !it.lowercase().startsWith("download") && !it.lowercase().startsWith("volume") && !it.lowercase().startsWith("chapter") }
break addAll(contents)
}
} }
} }
return@let "Sinopsis:\n" + sinopsisTexts.joinToString("\n\n") if (sinopsisTexts.isNotEmpty()) {
val isChapterList = sinopsisTexts.first().matches(chapterPrefixRegex)
val prefix = if (isChapterList) "Daftar Chapter:" else "Sinopsis:"
return@let "$prefix\n" + sinopsisTexts.joinToString("\n\n")
}
} }
// CASE 4: Satu paragraf saja dengan <strong> dan <br> // CASE 4: Satu paragraf saja dengan <strong> dan <br> (Manhwa Style)
if ( if (
paragraphs.size == 1 && paragraphs.size == 1 &&
element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))").isNotEmpty() element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))").isNotEmpty()
@ -546,16 +543,20 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
val para = paragraphs[0] val para = paragraphs[0]
val htmlSplit = para.html().split("<br>") val htmlSplit = para.html().split("<br>")
val content = htmlSplit.getOrNull(1)?.replace(htmlTagRegex, "")?.trim().orEmpty() val content = htmlSplit.getOrNull(1)?.let {
decodeHtmlEntities(it.replace(htmlTagRegex, "").trim())
}.orEmpty()
return@let "Sinopsis:\n$content" if (content.isNotBlank()) {
return@let "Sinopsis:\n$content"
}
} }
// CASE 5: Fallback // CASE 6: Fallback
if (firstText == "sinopsis:") { if (firstText == "sinopsis:") {
val sinopsisLines = paragraphs.drop(1) val sinopsisLines = paragraphs.drop(1)
.map { it.text().trim() } .map { decodeHtmlEntities(it.text().trim()) }
.filter { !it.lowercase().startsWith("download") } .filter { it.isNotEmpty() && !it.lowercase().startsWith("download") && !it.lowercase().startsWith("volume") && !it.lowercase().startsWith("chapter") }
return@let "Sinopsis:\n" + sinopsisLines.joinToString("\n\n") return@let "Sinopsis:\n" + sinopsisLines.joinToString("\n\n")
} }