Doujindesu: Add more filter and fix description (#8405)

* [WIP] DoujinDesu Add Group and Series Filter

https://github.com/keiyoushi/extensions-source/issues/7816

* Remove Unused Feature

* Bump version

* Add Class

* Rephrase

* Progress today

* Fixing errors that were not fixed yesterday

* Separate Author Filter same as Group and Series

* Update again about the desc

* Uhh my left over

* Changes requested

Thanks to @AwkwardPeak7

* I left it

* Don't use Author, Group, and Series filter when query isn't blank
This commit is contained in:
TheKingTermux 2025-04-11 23:37:38 +07:00 committed by Draff
parent fceca33d30
commit b9a05d4fcc
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
2 changed files with 162 additions and 16 deletions

View File

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

View File

@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.getPreferencesLazy
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
@ -249,6 +250,8 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
) )
private class AuthorFilter : Filter.Text("Author") private class AuthorFilter : Filter.Text("Author")
private class GroupFilter : Filter.Text("Group")
private class SeriesFilter : Filter.Text("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)
@ -317,8 +320,10 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
// Search & FIlter // Search & FIlter
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// Anything else filter handling
val url = "$baseUrl/manga/page/$page/".toHttpUrl().newBuilder() val url = "$baseUrl/manga/page/$page/".toHttpUrl().newBuilder()
.addQueryParameter("title", query) url.addQueryParameter("title", query.ifBlank { "" })
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) { when (filter) {
is CategoryNames -> { is CategoryNames -> {
@ -329,19 +334,14 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
val order = filter.values[filter.state] val order = filter.values[filter.state]
url.addQueryParameter("order", order.key) url.addQueryParameter("order", order.key)
} }
is AuthorFilter -> {
url.addQueryParameter("author", filter.state)
}
is CharacterFilter -> { is CharacterFilter -> {
url.addQueryParameter("character", filter.state) url.addQueryParameter("character", filter.state)
} }
is GenreList -> { is GenreList -> {
filter.state filter.state
.filter { it.state } .filter { it.state }
.let { list -> .forEach { genre ->
if (list.isNotEmpty()) { url.addQueryParameter("genre[]", genre.id)
list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) }
}
} }
} }
is StatusList -> { is StatusList -> {
@ -352,16 +352,76 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
} }
} }
val author = filters.firstInstanceOrNull<AuthorFilter>()?.state?.trim()
val group = filters.firstInstanceOrNull<GroupFilter>()?.state?.trim()
val series = filters.firstInstanceOrNull<SeriesFilter>()?.state?.trim()
// Author filter handling
if (query.isBlank()) {
if (!author.isNullOrBlank()) {
val slug = author.toMultiSlug()
if (slug.isNotBlank()) {
val authorUrl = if (page == 1) {
"$baseUrl/author/$slug/"
} else {
"$baseUrl/author/$slug/page/$page/"
}
return GET(authorUrl, headers)
}
}
// Group filter handling
if (!group.isNullOrBlank()) {
val slug = group.toMultiSlug()
if (slug.isNotBlank()) {
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)
}
}
}
return GET(url.build(), headers) return GET(url.build(), 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!"), Filter.Header("NB: Filter bisa digabungkan dengan memakai pencarian teks selain Author, Group dan Series!"),
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!"),
AuthorFilter(), AuthorFilter(),
GroupFilter(),
SeriesFilter(),
Filter.Separator(),
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),
@ -371,6 +431,9 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
// Detail Parse // Detail Parse
private val chapterListRegex = Regex("""\d+[-]?\d*\..+<br>""", RegexOption.IGNORE_CASE)
private val htmlTagRegex = Regex("<[^>]*>")
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.selectFirst("section.metadata")!! val infoElement = document.selectFirst("section.metadata")!!
val authorName = if (infoElement.select("td:contains(Author) ~ td").isEmpty()) { val authorName = if (infoElement.select("td:contains(Author) ~ td").isEmpty()) {
@ -414,13 +477,96 @@ class DoujinDesu : ParsedHttpSource(), ConfigurableSource {
Seri : $seriesParser Seri : $seriesParser
""".trimIndent() """.trimIndent()
} else { } else {
val showDescription = infoElement.selectFirst("div.pb-2 > p:nth-child(1)")!!.text() val pb2Element = infoElement.selectFirst("div.pb-2")
"""
$showDescription
Judul Alternatif : $alternativeTitle val showDescription = pb2Element?.let { element ->
Seri : $seriesParser val paragraphs = element.select("p")
""".trimIndent() val firstText = paragraphs.firstOrNull()?.text()?.trim()?.lowercase()
// CASE 1: Gabungan chapter dalam satu paragraf
val mergedChapterElement = element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))").firstOrNull {
chapterListRegex.containsMatchIn(it.html())
}
if (mergedChapterElement != null) {
val chapterList = mergedChapterElement.html()
.split("<br>")
.drop(1)
.map { it.replace(htmlTagRegex, "").trim() }
.filter { it.isNotEmpty() }
return@let "Daftar Chapter:\n" + chapterList.joinToString(" | ")
}
// CASE 2: Dua paragraf: p[0] = "Sinopsis:", p[1] = daftar chapter
if (
firstText == "sinopsis:" &&
paragraphs.size > 1 &&
chapterListRegex.containsMatchIn(paragraphs[1].html())
) {
val chapterList = paragraphs[1].html()
.split("<br>")
.map { it.replace(htmlTagRegex, "").trim() }
.filter { it.isNotEmpty() }
return@let "Daftar Chapter:\n" + chapterList.joinToString(" | ")
}
// CASE 3: Sinopsis biasa pakai <strong>Sinopsis:</strong> di p awal
val sinopsisPara = element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))")
if (sinopsisPara.isNotEmpty()) {
val sinopsisStart = sinopsisPara.first()!!
val htmlSplit = sinopsisStart.html().split("<br>")
val startText = htmlSplit.getOrNull(1)?.replace(htmlTagRegex, "")?.trim().orEmpty()
val sinopsisTexts = buildList {
if (startText.isNotEmpty()) add(startText)
val allP = element.select("p")
val startIndex = allP.indexOf(sinopsisStart)
for (i in startIndex + 1 until allP.size) {
val content = allP[i].text().trim()
if (!content.lowercase().startsWith("download")) {
add(content)
} else {
break
}
}
}
return@let "Sinopsis:\n" + sinopsisTexts.joinToString("\n\n")
}
// CASE 4: Satu paragraf saja dengan <strong> dan <br>
if (
paragraphs.size == 1 &&
element.select("p:has(strong:matchesOwn(^\\s*Sinopsis\\s*:))").isNotEmpty()
) {
val para = paragraphs[0]
val htmlSplit = para.html().split("<br>")
val content = htmlSplit.getOrNull(1)?.replace(htmlTagRegex, "")?.trim().orEmpty()
return@let "Sinopsis:\n$content"
}
// CASE 5: Fallback
if (firstText == "sinopsis:") {
val sinopsisLines = paragraphs.drop(1)
.map { it.text().trim() }
.filter { !it.lowercase().startsWith("download") }
return@let "Sinopsis:\n" + sinopsisLines.joinToString("\n\n")
}
return@let ""
} ?: ""
"""
|$showDescription
|
|Judul Alternatif : $alternativeTitle
|Seri : $seriesParser
""".trimMargin().replace(Regex(" +"), " ")
} }
val genres = mutableListOf<String>() val genres = mutableListOf<String>()
infoElement.select("div.tags > a").forEach { element -> infoElement.select("div.tags > a").forEach { element ->