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'
|
extClass = '.MangaTR'
|
||||||
themePkg = 'fmreader'
|
themePkg = 'fmreader'
|
||||||
baseUrl = 'https://manga-tr.com'
|
baseUrl = 'https://manga-tr.com'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.extension.tr.mangatr
|
package eu.kanade.tachiyomi.extension.tr.mangatr
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.firstInstanceOrNull
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@ -16,6 +20,8 @@ import okhttp3.Response
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class MangaTR : FMReader("Manga-TR", "https://manga-tr.com", "tr") {
|
class MangaTR : FMReader("Manga-TR", "https://manga-tr.com", "tr") {
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
@ -32,24 +38,106 @@ class MangaTR : FMReader("Manga-TR", "https://manga-tr.com", "tr") {
|
|||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
override fun popularMangaNextPageSelector() = "div.btn-group:not(div.btn-block) button.btn-info"
|
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 ===============================
|
// =============================== Search ===============================
|
||||||
// TODO: genre search possible but a bit of a pain
|
// Dynamic genre list cache
|
||||||
override fun getFilterList() = FilterList()
|
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 {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl/arama.html".toHttpUrl().newBuilder()
|
if (query.isNotBlank()) {
|
||||||
.addQueryParameter("icerik", query)
|
val url = "$baseUrl/arama.html".toHttpUrl().newBuilder()
|
||||||
.build()
|
.addQueryParameter("icerik", query)
|
||||||
return GET(url, headers)
|
.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 {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val mangas = response.use { it.asJsoup() }
|
val path = response.request.url.encodedPath
|
||||||
.select("div.row a[data-toggle]")
|
return if (path.contains("/arama.html")) {
|
||||||
.filterNot { it.siblingElements().text().contains("Novel") }
|
val mangas = response.asJsoup()
|
||||||
.map(::searchMangaFromElement)
|
.select("div.row a[data-toggle]")
|
||||||
|
.filterNot { it.siblingElements().text().contains("Novel") }
|
||||||
return MangasPage(mangas, false)
|
.map(::searchMangaFromElement)
|
||||||
|
MangasPage(mangas, false)
|
||||||
|
} else {
|
||||||
|
super.searchMangaParse(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
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) =
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
GET("$baseUrl/${chapter.url.substringAfter("cek/")}", headers)
|
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