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:
Hasan 2025-08-11 05:49:09 +03:00 committed by Draff
parent 45f31c3b75
commit c2317eeeed
Signed by: Draff
GPG Key ID: E8A89F3211677653
2 changed files with 227 additions and 13 deletions

View File

@ -3,7 +3,7 @@ ext {
extClass = '.MangaTR'
themePkg = 'fmreader'
baseUrl = 'https://manga-tr.com'
overrideVersionCode = 4
overrideVersionCode = 5
isNsfw = true
}

View File

@ -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"),
)
}