Manga-TR: Add filter support and fix the image could not be loaded error (#10055)
* Add filter support and fix the image could not be loaded error Add dynamic Genres from manga-list.html (#genreSelect), send as repeated genre[]=<value> Add filters: Publication (durum), Translation (ceviri), Age (yas), Content Type (icerik), Special Type (tur) Fix WebView images by resolving Base64-encoded data-src with fallbacks * Apply requested changes
This commit is contained in:
parent
45f31c3b75
commit
c2317eeeed
@ -3,7 +3,7 @@ ext {
|
||||
extClass = '.MangaTR'
|
||||
themePkg = 'fmreader'
|
||||
baseUrl = 'https://manga-tr.com'
|
||||
overrideVersionCode = 4
|
||||
overrideVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,18 @@
|
||||
package eu.kanade.tachiyomi.extension.tr.mangatr
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
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.util.asJsoup
|
||||
import keiyoushi.utils.firstInstanceOrNull
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
@ -16,6 +20,8 @@ import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class MangaTR : FMReader("Manga-TR", "https://manga-tr.com", "tr") {
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
@ -32,24 +38,106 @@ class MangaTR : FMReader("Manga-TR", "https://manga-tr.com", "tr") {
|
||||
// ============================== Popular ===============================
|
||||
override fun popularMangaNextPageSelector() = "div.btn-group:not(div.btn-block) button.btn-info"
|
||||
|
||||
override fun popularMangaSelector() = "div.row a[data-toggle]"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
title = element.text()
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
// TODO: genre search possible but a bit of a pain
|
||||
override fun getFilterList() = FilterList()
|
||||
// Dynamic genre list cache
|
||||
private var cachedGenres: List<FMReader.Genre> = emptyList()
|
||||
|
||||
@Volatile private var isLoadingGenres: Boolean = false
|
||||
|
||||
// Filters UI: site-specific filters + dynamically fetched genres
|
||||
override fun getFilterList(): FilterList {
|
||||
loadGenresAsync()
|
||||
val baseFilters = mutableListOf<Filter<*>>(
|
||||
Filter.Header("Metin araması ile filtreler birlikte kullanılmaz"),
|
||||
PublicationStatusFilter(),
|
||||
TranslateStatusFilter(),
|
||||
AgeRestrictionFilter(),
|
||||
ContentTypeFilter(),
|
||||
SpecialTypeFilter(),
|
||||
)
|
||||
|
||||
if (cachedGenres.isNotEmpty()) {
|
||||
baseFilters += FMReader.GenreList(cachedGenres)
|
||||
} else {
|
||||
baseFilters += Filter.Header("Türleri yüklemek için 'Sıfırla' düğmesine basın")
|
||||
}
|
||||
|
||||
return FilterList(baseFilters)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/arama.html".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("icerik", query)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
if (query.isNotBlank()) {
|
||||
val url = "$baseUrl/arama.html".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("icerik", query)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
val listEndpoint = if (page <= 1) "$baseUrl/manga-list.html" else "$baseUrl/$requestPath"
|
||||
val url = listEndpoint.toHttpUrl().newBuilder()
|
||||
if (page > 1) {
|
||||
url.addQueryParameter("listType", "pagination")
|
||||
url.addQueryParameter("page", page.toString())
|
||||
}
|
||||
|
||||
val genreFilter = filters.firstInstanceOrNull<FMReader.GenreList>()
|
||||
|
||||
if (genreFilter != null && genreFilter.state.isNotEmpty()) {
|
||||
val included = genreFilter.state.filter { it.isIncluded() }
|
||||
if (included.isNotEmpty()) {
|
||||
included.forEach { url.addQueryParameter("genre[]", it.id) }
|
||||
}
|
||||
}
|
||||
|
||||
// Diğer filtreler
|
||||
val filterList = filters
|
||||
|
||||
filterList.firstInstanceOrNull<PublicationStatusFilter>()?.let { f ->
|
||||
val value = arrayOf("", "1", "2")[f.state]
|
||||
if (value.isNotEmpty()) url.addQueryParameter("durum", value)
|
||||
}
|
||||
|
||||
filterList.firstInstanceOrNull<TranslateStatusFilter>()?.let { f ->
|
||||
val value = arrayOf("", "1", "2", "3", "4")[f.state]
|
||||
if (value.isNotEmpty()) url.addQueryParameter("ceviri", value)
|
||||
}
|
||||
|
||||
filterList.firstInstanceOrNull<AgeRestrictionFilter>()?.let { f ->
|
||||
val value = arrayOf("", "16", "18")[f.state]
|
||||
if (value.isNotEmpty()) url.addQueryParameter("yas", value)
|
||||
}
|
||||
|
||||
filterList.firstInstanceOrNull<ContentTypeFilter>()?.let { f ->
|
||||
val value = arrayOf("", "1", "2", "3", "4")[f.state]
|
||||
if (value.isNotEmpty()) url.addQueryParameter("icerik", value)
|
||||
}
|
||||
|
||||
filterList.firstInstanceOrNull<SpecialTypeFilter>()?.let { f ->
|
||||
val value = arrayOf("", "2")[f.state]
|
||||
if (value.isNotEmpty()) url.addQueryParameter("tur", value)
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangas = response.use { it.asJsoup() }
|
||||
.select("div.row a[data-toggle]")
|
||||
.filterNot { it.siblingElements().text().contains("Novel") }
|
||||
.map(::searchMangaFromElement)
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
val path = response.request.url.encodedPath
|
||||
return if (path.contains("/arama.html")) {
|
||||
val mangas = response.asJsoup()
|
||||
.select("div.row a[data-toggle]")
|
||||
.filterNot { it.siblingElements().text().contains("Novel") }
|
||||
.map(::searchMangaFromElement)
|
||||
MangasPage(mangas, false)
|
||||
} else {
|
||||
super.searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
@ -116,4 +204,130 @@ class MangaTR : FMReader("Manga-TR", "https://manga-tr.com", "tr") {
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET("$baseUrl/${chapter.url.substringAfter("cek/")}", headers)
|
||||
|
||||
override val pageListImageSelector = "div.chapter-content img.chapter-img"
|
||||
|
||||
// Manga-TR: image URL resolution with Base64-decoded data-src
|
||||
override fun getImgAttr(element: Element?): String? {
|
||||
return when {
|
||||
element == null -> null
|
||||
element.hasAttr("data-src") -> {
|
||||
try {
|
||||
val encodedUrl = element.attr("data-src")
|
||||
val decodedBytes = Base64.decode(encodedUrl, Base64.DEFAULT)
|
||||
String(decodedBytes, StandardCharsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
element.attr("abs:src")
|
||||
}
|
||||
}
|
||||
element.hasAttr("src") -> element.attr("abs:src")
|
||||
element.hasAttr("data-original") -> element.attr("abs:data-original")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
// Simple pageListParse - relies on the selector above
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(pageListImageSelector).mapIndexed { i, img ->
|
||||
Page(i, imageUrl = getImgAttr(img))
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== List Parse ===========================
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
// Parse genres from the current response to avoid extra requests
|
||||
if (cachedGenres.isEmpty()) {
|
||||
val options = document.select("#genreSelect option")
|
||||
if (options.isNotEmpty()) {
|
||||
cachedGenres = options.mapNotNull { opt ->
|
||||
val value = opt.attr("value").trim()
|
||||
val text = opt.text().trim()
|
||||
if (text.isEmpty() || value.isEmpty()) null else FMReader.Genre(text, value)
|
||||
}
|
||||
} else {
|
||||
val container = document.selectFirst("*:matchesOwn(Tür Seçiniz)")?.parent()
|
||||
?: document.selectFirst("div:has(:matchesOwn(Tür Seçiniz))")
|
||||
val anchors = container?.select("a")
|
||||
?: document.select("a[href*=manga-list], a[href*=genre], a[href*=tur]")
|
||||
val items = anchors.map { it.text().trim() }.filter { it.length > 1 }.distinct()
|
||||
if (items.isNotEmpty()) {
|
||||
cachedGenres = items.map { name -> FMReader.Genre(name, name.replace(' ', '+')) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { popularMangaFromElement(it) }
|
||||
|
||||
val hasNextPage = (document.select(popularMangaNextPageSelector()).first()?.text() ?: "").let {
|
||||
if (it.contains(Regex("""\w*\s\d*\s\w*\s\d*"""))) {
|
||||
it.split(" ").let { pageOf -> pageOf[1] != pageOf[3] }
|
||||
} else {
|
||||
it.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun loadGenresAsync() {
|
||||
if (cachedGenres.isNotEmpty() || isLoadingGenres) return
|
||||
isLoadingGenres = true
|
||||
thread(name = "mangatr-load-genres", start = true) {
|
||||
try {
|
||||
val doc = client.newCall(GET("$baseUrl/manga-list.html", headers)).execute().asJsoup()
|
||||
|
||||
val options = doc.select("#genreSelect option")
|
||||
if (options.isNotEmpty()) {
|
||||
cachedGenres = options.mapNotNull { opt ->
|
||||
val value = opt.attr("value").trim()
|
||||
val text = opt.text().trim()
|
||||
if (text.isEmpty() || value.isEmpty()) null else FMReader.Genre(text, value)
|
||||
}
|
||||
return@thread
|
||||
}
|
||||
|
||||
val container = doc.selectFirst("*:matchesOwn(Tür Seçiniz)")?.parent()
|
||||
?: doc.selectFirst("div:has(:matchesOwn(Tür Seçiniz))")
|
||||
val anchors = when {
|
||||
container != null -> container.select("a")
|
||||
else -> doc.select("a[href*=manga-list], a[href*=genre], a[href*=tur]")
|
||||
}
|
||||
val items = anchors.map { it.text().trim() }.filter { it.length > 1 }.distinct()
|
||||
if (items.isNotEmpty()) {
|
||||
cachedGenres = items.map { name -> FMReader.Genre(name, name.replace(' ', '+')) }
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
} finally {
|
||||
isLoadingGenres = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== Filters (UI) ===========================
|
||||
private class PublicationStatusFilter : Filter.Select<String>(
|
||||
"Yayın Durumu",
|
||||
arrayOf("Tümü", "Tamamlandı", "Devam Ediyor"),
|
||||
)
|
||||
|
||||
private class TranslateStatusFilter : Filter.Select<String>(
|
||||
"Çeviri Durumu",
|
||||
arrayOf("Tümü", "Devam Ediyor", "Tamamlandı", "Bırakılmış", "Yok"),
|
||||
)
|
||||
|
||||
private class AgeRestrictionFilter : Filter.Select<String>(
|
||||
"Yaş Sınırlaması",
|
||||
arrayOf("Tümü", "16+", "18+"),
|
||||
)
|
||||
|
||||
private class ContentTypeFilter : Filter.Select<String>(
|
||||
"İçerik Türü",
|
||||
arrayOf("Tümü", "Manga", "Novel", "Webtoon", "Anime"),
|
||||
)
|
||||
|
||||
private class SpecialTypeFilter : Filter.Select<String>(
|
||||
"Özel Tür",
|
||||
arrayOf("Tümü", "Yetişkin"),
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user