From c2317eeeed3fec12b82e5ace0b061efe6646d0ee Mon Sep 17 00:00:00 2001 From: Hasan Date: Mon, 11 Aug 2025 05:49:09 +0300 Subject: [PATCH] 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[]= 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 --- src/tr/mangatr/build.gradle | 2 +- .../tachiyomi/extension/tr/mangatr/MangaTR.kt | 238 +++++++++++++++++- 2 files changed, 227 insertions(+), 13 deletions(-) diff --git a/src/tr/mangatr/build.gradle b/src/tr/mangatr/build.gradle index 9905f4275..e6e8fb6c4 100644 --- a/src/tr/mangatr/build.gradle +++ b/src/tr/mangatr/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.MangaTR' themePkg = 'fmreader' baseUrl = 'https://manga-tr.com' - overrideVersionCode = 4 + overrideVersionCode = 5 isNsfw = true } diff --git a/src/tr/mangatr/src/eu/kanade/tachiyomi/extension/tr/mangatr/MangaTR.kt b/src/tr/mangatr/src/eu/kanade/tachiyomi/extension/tr/mangatr/MangaTR.kt index 4a1a57a19..f65da93a0 100644 --- a/src/tr/mangatr/src/eu/kanade/tachiyomi/extension/tr/mangatr/MangaTR.kt +++ b/src/tr/mangatr/src/eu/kanade/tachiyomi/extension/tr/mangatr/MangaTR.kt @@ -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 = emptyList() + + @Volatile private var isLoadingGenres: Boolean = false + + // Filters UI: site-specific filters + dynamically fetched genres + override fun getFilterList(): FilterList { + loadGenresAsync() + val baseFilters = mutableListOf>( + 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() + + 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()?.let { f -> + val value = arrayOf("", "1", "2")[f.state] + if (value.isNotEmpty()) url.addQueryParameter("durum", value) + } + + filterList.firstInstanceOrNull()?.let { f -> + val value = arrayOf("", "1", "2", "3", "4")[f.state] + if (value.isNotEmpty()) url.addQueryParameter("ceviri", value) + } + + filterList.firstInstanceOrNull()?.let { f -> + val value = arrayOf("", "16", "18")[f.state] + if (value.isNotEmpty()) url.addQueryParameter("yas", value) + } + + filterList.firstInstanceOrNull()?.let { f -> + val value = arrayOf("", "1", "2", "3", "4")[f.state] + if (value.isNotEmpty()) url.addQueryParameter("icerik", value) + } + + filterList.firstInstanceOrNull()?.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 { + 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( + "Yayın Durumu", + arrayOf("Tümü", "Tamamlandı", "Devam Ediyor"), + ) + + private class TranslateStatusFilter : Filter.Select( + "Çeviri Durumu", + arrayOf("Tümü", "Devam Ediyor", "Tamamlandı", "Bırakılmış", "Yok"), + ) + + private class AgeRestrictionFilter : Filter.Select( + "Yaş Sınırlaması", + arrayOf("Tümü", "16+", "18+"), + ) + + private class ContentTypeFilter : Filter.Select( + "İçerik Türü", + arrayOf("Tümü", "Manga", "Novel", "Webtoon", "Anime"), + ) + + private class SpecialTypeFilter : Filter.Select( + "Özel Tür", + arrayOf("Tümü", "Yetişkin"), + ) }