MangaDemon: Update to new site structure (#4857)

* fix

* bruh
This commit is contained in:
bapeey 2024-08-30 23:30:53 -05:00 committed by Draff
parent e11342f5df
commit 6b4dbb1d3d
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 70 additions and 180 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Manga Demon' extName = 'Manga Demon'
extClass = '.MangaDemon' extClass = '.MangaDemon'
extVersionCode = 12 extVersionCode = 13
isNsfw = false isNsfw = false
} }

View File

@ -9,129 +9,62 @@ 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.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
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.io.IOException import java.text.ParseException
import java.net.URLEncoder
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class MangaDemon : ParsedHttpSource() { class MangaDemon : ParsedHttpSource() {
override val versionId = 2
override val lang = "en" override val lang = "en"
override val supportsLatest = true override val supportsLatest = true
override val name = "Manga Demon" override val name = "Manga Demon"
override val baseUrl = "https://mgdemon.org" override val baseUrl = "https://demonicscans.org"
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient.newBuilder()
.rateLimit(1) .rateLimit(1)
.addInterceptor { chain ->
val request = chain.request()
val headers = request.headers.newBuilder()
.removeAll("Accept-Encoding")
.build() .build()
chain.proceed(request.newBuilder().headers(headers).build())
}
.addInterceptor(::dynamicUrlInterceptor)
.build()
// Cache suffix
private var dynamicUrlSuffix = ""
private var dynamicUrlSuffixUpdated: Long = 0
private val dynamicUrlSuffixValidity: Long = 10 * 60 // 10 minutes
private fun dynamicUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val timeNow = System.currentTimeMillis() / 1000
// Check if request requires an up-to-date suffix
if (request.url.pathSegments[0] == "manga") {
// Force update suffix if required
if (timeNow - dynamicUrlSuffixUpdated > dynamicUrlSuffixValidity) {
client.newCall(GET(baseUrl)).execute()
if (timeNow - dynamicUrlSuffixUpdated > dynamicUrlSuffixValidity) {
throw IOException("Failed to update dynamic url suffix")
}
}
val newPath = request.url
.encodedPath
.replaceAfterLast("-", dynamicUrlSuffix)
val newUrl = request.url.newBuilder()
.encodedPath(newPath)
.build()
val newRequest = request.newBuilder()
.url(newUrl)
.build()
return chain.proceed(newRequest)
}
// Always update suffix
val response = chain.proceed(request)
val document = Jsoup.parse(
response.peekBody(Long.MAX_VALUE).string(),
request.url.toString(),
)
val links = document.select("a[href^='/manga/']")
// Get the most popular suffix after last `-`
val suffix = links.map { it.attr("href").substringAfterLast("-") }
.groupBy { it }
.maxByOrNull { it.value.size }
?.key
if (suffix != null) {
dynamicUrlSuffix = suffix
dynamicUrlSuffixUpdated = timeNow
}
return response
}
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl) .add("Referer", "$baseUrl/")
// latest override fun popularMangaRequest(page: Int): Request {
override fun latestUpdatesRequest(page: Int): Request { return GET("$baseUrl/advanced.php?list=$page&status=all&orderby=VIEWS%20DESC", headers)
return GET("$baseUrl/updates.php?list=$page", headers)
} }
override fun latestUpdatesNextPageSelector() = ".pagination a:contains(Next)" override fun popularMangaNextPageSelector() = "div.pagination > ul > a > li:contains(Next)"
override fun latestUpdatesSelector() = "div.leftside" override fun popularMangaSelector() = "div#advanced-content > div.advanced-element"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
title = element.selectFirst("h1")!!.ownText()
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/lastupdates.php?list=$page", headers)
}
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = "div#updates-container > div.updates-element"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
element.select("a").apply { with(element.selectFirst("div.updates-element-info")!!) {
title = attr("title") setUrlWithoutDomain(selectFirst("a")!!.attr("abs:href"))
val url = URLEncoder.encode(attr("href"), "UTF-8") title = selectFirst("a")!!.ownText()
setUrlWithoutDomain(url)
} }
thumbnail_url = element.select("img").attr("abs:src") thumbnail_url = element.selectFirst("div.thumb img")!!.attr("abs:src")
} }
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse.php?list=$page", headers)
}
override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun popularMangaSelector() = latestUpdatesSelector()
override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element)
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.isNotEmpty()) { return if (query.isNotEmpty()) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
@ -142,8 +75,25 @@ class MangaDemon : ParsedHttpSource() {
} }
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/search.php".toHttpUrl().newBuilder()
.addQueryParameter("manga", query)
.build()
return GET(url, headers)
}
override fun searchMangaSelector() = "body > a[href]"
override fun searchMangaNextPageSelector() = null
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
title = element.selectFirst("div.seach-right > div")!!.ownText()
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
}
private fun filterSearchRequest(page: Int, filters: FilterList): Request { private fun filterSearchRequest(page: Int, filters: FilterList): Request {
val url = "$baseUrl/browse.php".toHttpUrl().newBuilder().apply { val url = "$baseUrl/advanced.php".toHttpUrl().newBuilder().apply {
addQueryParameter("list", page.toString()) addQueryParameter("list", page.toString())
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
@ -170,36 +120,14 @@ class MangaDemon : ParsedHttpSource() {
override fun getFilterList() = getFilters() override fun getFilterList() = getFilters()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val url = "$baseUrl/search.php".toHttpUrl().newBuilder() with(document.selectFirst("div#manga-info-container")!!) {
.addQueryParameter("manga", query) title = selectFirst("h1.big-fat-titles")!!.ownText()
.build() thumbnail_url = selectFirst("div#manga-page img")!!.attr("abs:src")
return GET(url, headers) genre = select("div.genres-list > li").joinToString { it.text() }
} description = selectFirst("div#manga-info-rightColumn > div > div.white-font")!!.text()
author = select("div#manga-info-stats > div:has(> li:eq(0):contains(Author)) > li:eq(1)").text()
override fun searchMangaSelector() = "a.boxsizing" status = parseStatus(select("div#manga-info-stats > div:has(> li:eq(0):contains(Status)) > li:eq(1)").text())
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.text()
val url = URLEncoder.encode(element.attr("href"), "UTF-8")
setUrlWithoutDomain(url)
val urlSorter = title.replace(":", "%20")
thumbnail_url = ("https://readermc.org/images/thumbnails/$urlSorter.webp")
}
override fun searchMangaNextPageSelector() = null
// Manga details
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("article")
return SManga.create().apply {
title = infoElement.select("h1.novel-title").text()
author = infoElement.select("div.author > [itemprop=author]").text()
status = parseStatus(infoElement.select("span:has(small:containsOwn(Status))").text())
genre = infoElement.select("a.property-item").joinToString { it.text() }
description = infoElement.select("p.description").text()
thumbnail_url = infoElement.select("img#thumbonail").attr("src")
} }
} }
@ -210,68 +138,31 @@ class MangaDemon : ParsedHttpSource() {
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListSelector() = "ul.chapter-list li" override fun chapterListSelector() = "div#chapters-list a.chplinks"
// Get Chapters override fun chapterFromElement(element: Element) = SChapter.create().apply {
override fun chapterFromElement(element: Element): SChapter { setUrlWithoutDomain(element.attr("abs:href"))
return SChapter.create().apply { name = element.ownText()
element.select("a").let { urlElement -> date_upload = parseDate(element.selectFirst("span")?.ownText())
val url = URLEncoder.encode(urlElement.attr("href"), "UTF-8")
setUrlWithoutDomain(url)
name = element.select("strong.chapter-title").text()
}
val date = element.select("time.chapter-update").text()
date_upload = parseDate(date)
}
} }
private fun parseDate(dateStr: String): Long { private fun parseDate(dateStr: String?): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time } return try {
.getOrNull() ?: 0L dateStr?.let { DATE_FORMATTER.parse(it)?.time } ?: 0
} catch (_: ParseException) {
0L
} }
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
private val loadMoreEndpointRegex by lazy { Regex("""GET[^/]+([^=]+)""") }
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val baseImages = document.select("img.imgholder") return document.select("img.imgholder").mapIndexed { i, element ->
.map { it.attr("abs:src") } Page(i, "", element.attr("abs:src"))
.toMutableList()
baseImages.addAll(loadMoreImages(document))
return baseImages.mapIndexed { i, img -> Page(i, "", img) }
} }
private fun loadMoreImages(document: Document): List<String> {
val buttonHtml = document.selectFirst("img.imgholder ~ button")
?.attr("onclick")?.replace("\"", "\'")
?: return emptyList()
val id = buttonHtml.substringAfter("\'").substringBefore("\'").trim()
val funcName = buttonHtml.substringBefore("(").trim()
val endpoint = document.selectFirst("script:containsData($funcName)")
?.data()
?.let { loadMoreEndpointRegex.find(it)?.groupValues?.get(1) }
?: return emptyList()
val response = client.newCall(GET("$baseUrl$endpoint=$id", headers)).execute()
if (!response.isSuccessful) {
response.close()
return emptyList()
}
return response.use { it.asJsoup() }
.select("img")
.map { it.attr("abs:src") }
} }
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
companion object {
private val DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
} }

View File

@ -45,7 +45,6 @@ class GenreFilter : Filter.Group<CheckBoxFilter>(
companion object { companion object {
private val genres = listOf( private val genres = listOf(
Pair("All", "all"),
Pair("Action", "1"), Pair("Action", "1"),
Pair("Adventure", "2"), Pair("Adventure", "2"),
Pair("Comedy", "3"), Pair("Comedy", "3"),