Fix AsuraScans (#4198)

* basics

* bruh

* Add filters

* they will need to migrate

* cloudflareClient

* dynamic url

* remove old prefences

* rename function

* automigration?

* bruh2

* Apply review

* bruh3

* a
This commit is contained in:
bapeey 2024-07-25 10:54:02 -05:00 committed by GitHub
parent 86f539c3e7
commit afeea1ad44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 310 additions and 41 deletions

View File

@ -1,9 +1,7 @@
ext { ext {
extName = 'Asura Scans' extName = 'Asura Scans'
extClass = '.AsuraScans' extClass = '.AsuraScans'
themePkg = 'mangathemesia' extVersionCode = 35
baseUrl = 'https://asuracomic.net'
overrideVersionCode = 4
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,21 +1,50 @@
package eu.kanade.tachiyomi.extension.en.asurascans package eu.kanade.tachiyomi.extension.en.asurascans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
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.Page 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.source.online.ParsedHttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread
class AsuraScans : ParsedHttpSource(), ConfigurableSource {
override val name = "Asura Scans"
override val baseUrl = "https://asuracomic.net"
private val apiUrl = "https://gg.asuracomic.net/api"
override val lang = "en"
override val supportsLatest = true
private val dateFormat = SimpleDateFormat("MMMM d yyyy", Locale.US)
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
class AsuraScans : MangaThemesiaAlt(
"Asura Scans",
"https://asuracomic.net",
"en",
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
randomUrlPrefKey = "pref_permanent_manga_url_2_en",
) {
init { init {
// remove legacy preferences // remove legacy preferences
preferences.run { preferences.run {
@ -25,47 +54,256 @@ class AsuraScans : MangaThemesiaAlt(
if (contains("pref_base_url_host")) { if (contains("pref_base_url_host")) {
edit().remove("pref_base_url_host").apply() edit().remove("pref_base_url_host").apply()
} }
} if (contains("pref_permanent_manga_url_2_en")) {
} edit().remove("pref_permanent_manga_url_2_en").apply()
override val client = super.client.newBuilder()
.rateLimit(1, 3)
.apply {
val interceptors = interceptors()
val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName }
if (index >= 0) {
interceptors.add(interceptors.removeAt(index))
} }
} }
}
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1, 3)
.build() .build()
override val seriesDescriptionSelector = "div.desc p, div.entry-content p, div[itemprop=description]:not(:has(p))" override fun headersBuilder() = super.headersBuilder()
override val seriesArtistSelector = ".fmed b:contains(artist)+span, .infox span:contains(artist)" .add("Referer", "$baseUrl/")
override val seriesAuthorSelector = ".fmed b:contains(author)+span, .infox span:contains(author)"
override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " + override fun popularMangaRequest(page: Int): Request =
"div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img" GET("$baseUrl/series?genres=&status=-1&types=-1&order=rating&page=$page", headers)
override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/series?genres=&status=-1&types=-1&order=update&page=$page", headers)
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = super.searchMangaRequest(page, query, filters) val url = "$baseUrl/series".toHttpUrl().newBuilder()
if (query.isBlank()) return request
val url = request.url.newBuilder() url.addQueryParameter("page", page.toString())
.addPathSegment("page/$page/")
.removeAllQueryParameters("page")
.removeAllQueryParameters("title")
.addQueryParameter("s", query)
.build()
return request.newBuilder() if (query.isNotBlank()) {
.url(url) url.addQueryParameter("name", query)
.build() }
val genres = filters.firstInstanceOrNull<GenreFilter>()?.state.orEmpty()
.filter(Genre::state)
.map(Genre::id)
.joinToString(",")
val status = filters.firstInstanceOrNull<StatusFilter>()?.toUriPart() ?: "-1"
val types = filters.firstInstanceOrNull<TypeFilter>()?.toUriPart() ?: "-1"
val order = filters.firstInstanceOrNull<OrderFilter>()?.toUriPart() ?: "rating"
url.addQueryParameter("genres", genres)
url.addQueryParameter("status", status)
url.addQueryParameter("types", types)
url.addQueryParameter("order", order)
return GET(url.build(), headers)
}
override fun searchMangaSelector() = "div.grid > a[href]"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("abs:href").toPermSlugIfNeeded())
title = element.selectFirst("div.block > span.block")!!.ownText()
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
override fun searchMangaNextPageSelector() = "div.flex > a.flex.bg-themecolor:contains(Next)"
override fun getFilterList(): FilterList {
fetchFilters()
val filters = mutableListOf<Filter<*>>()
if (filtersState == FiltersState.FETCHED) {
filters += listOf(
GenreFilter("Genres", getGenreFilters()),
StatusFilter("Status", getStatusFilters()),
TypeFilter("Types", getTypeFilters()),
)
} else {
filters += Filter.Header("Press 'Reset' to attempt to fetch the filters")
}
filters += OrderFilter(
"Order by",
listOf(
Pair("Rating", "rating"),
Pair("Update", "update"),
Pair("Latest", "latest"),
Pair("Z-A", "desc"),
Pair("A-Z", "asc"),
),
)
return FilterList(filters)
}
private fun getGenreFilters(): List<Genre> = genresList.map { Genre(it.first, it.second) }
private fun getStatusFilters(): List<Pair<String, String>> = statusesList.map { it.first to it.second.toString() }
private fun getTypeFilters(): List<Pair<String, String>> = typesList.map { it.first to it.second.toString() }
private var genresList: List<Pair<String, Int>> = emptyList()
private var statusesList: List<Pair<String, Int>> = emptyList()
private var typesList: List<Pair<String, Int>> = emptyList()
private var fetchFiltersAttempts = 0
private var filtersState = FiltersState.NOT_FETCHED
private fun fetchFilters() {
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
filtersState = FiltersState.FETCHING
fetchFiltersAttempts++
thread {
try {
val response = client.newCall(GET("$apiUrl/series/filters", headers)).execute()
val filters = json.decodeFromString<FiltersDto>(response.body.string())
genresList = filters.genres.filter { it.id > 0 }.map { it.name.trim() to it.id }
statusesList = filters.statuses.map { it.name.trim() to it.id }
typesList = filters.types.map { it.name.trim() to it.id }
filtersState = FiltersState.FETCHED
} catch (e: Throwable) {
filtersState = FiltersState.NOT_FETCHED
}
}
}
override fun mangaDetailsRequest(manga: SManga): Request {
if (!preferences.dynamicUrl()) return super.mangaDetailsRequest(manga)
val match = OLD_FORMAT_MANGA_REGEX.find(manga.url)?.groupValues?.get(2)
val slug = match ?: manga.url.substringAfter("/series/").substringBefore("/")
val savedSlug = preferences.slugMap[slug] ?: "$slug-"
return GET("$baseUrl/series/$savedSlug", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
if (preferences.dynamicUrl()) {
val url = response.request.url.toString()
val newSlug = url.substringAfter("/series/").substringBefore("/")
val absSlug = newSlug.substringBeforeLast("-")
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
}
return super.mangaDetailsParse(response)
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("span.text-xl.font-bold")!!.ownText()
thumbnail_url = document.selectFirst("img[alt=poster]")?.attr("abs:src")
description = document.selectFirst("span.font-medium.text-sm")?.text()
author = document.selectFirst("div.grid > div:has(h3:eq(0):containsOwn(Author)) > h3:eq(1)")?.ownText()
artist = document.selectFirst("div.grid > div:has(h3:eq(0):containsOwn(Artist)) > h3:eq(1)")?.ownText()
genre = document.select("div[class^=space] > div.flex > button.text-white").joinToString { it.ownText() }
status = parseStatus(document.selectFirst("div.flex:has(h3:eq(0):containsOwn(Status)) > h3:eq(1)")?.ownText())
}
private fun parseStatus(status: String?) = when (status) {
"Ongoing", "Season End" -> SManga.ONGOING
"Hiatus" -> SManga.ON_HIATUS
"Completed" -> SManga.COMPLETED
"Dropped" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
override fun chapterListParse(response: Response): List<SChapter> {
if (preferences.dynamicUrl()) {
val url = response.request.url.toString()
val newSlug = url.substringAfter("/series/").substringBefore("/")
val absSlug = newSlug.substringBeforeLast("-")
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
}
return super.chapterListParse(response)
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListSelector() = "div.scrollbar-thumb-themecolor > a.block"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("abs:href").toPermSlugIfNeeded())
name = element.selectFirst("h3:eq(0)")!!.ownText()
date_upload = try {
val text = element.selectFirst("h3:eq(1)")!!.ownText()
val cleanText = text.replace(CLEAN_DATE_REGEX, "$1")
dateFormat.parse(cleanText)?.time ?: 0
} catch (_: Exception) {
0L
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (!preferences.dynamicUrl()) return super.pageListRequest(chapter)
val match = OLD_FORMAT_CHAPTER_REGEX.containsMatchIn(chapter.url)
if (match) throw Exception("Please refresh the chapter list before reading.")
val slug = chapter.url.substringAfter("/series/").substringBefore("/")
val savedSlug = preferences.slugMap[slug] ?: "$slug-"
return GET(baseUrl + chapter.url.replace(slug, savedSlug), headers)
} }
// Skip scriptPages
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select(pageSelector) return document.select("div > img[alt=chapter]").mapIndexed { i, element ->
.filterNot { it.attr("src").isNullOrEmpty() } Page(i, imageUrl = element.attr("abs:src"))
.mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) } }
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
filterIsInstance<R>().firstOrNull()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_DYNAMIC_URL
title = "Automatically update dynamic URLs"
summary = "Automatically update random numbers in manga URLs.\nHelps mitigating HTTP 404 errors during update and \"in library\" marks when browsing.\nNote: This setting may require clearing database in advanced settings and migrating all manga to the same source."
setDefaultValue(true)
}.let(screen::addPreference)
}
private var SharedPreferences.slugMap: MutableMap<String, String>
get() {
val jsonMap = getString(PREF_SLUG_MAP, "{}")!!
return try {
json.decodeFromString<Map<String, String>>(jsonMap).toMutableMap()
} catch (_: Exception) {
mutableMapOf()
}
}
set(newSlugMap) {
edit()
.putString(PREF_SLUG_MAP, json.encodeToString(newSlugMap))
.apply()
}
private fun SharedPreferences.dynamicUrl(): Boolean = getBoolean(PREF_DYNAMIC_URL, true)
private fun String.toPermSlugIfNeeded(): String {
if (!preferences.dynamicUrl()) return this
val slug = this.substringAfter("/series/").substringBefore("/")
val absSlug = slug.substringBeforeLast("-")
preferences.slugMap = preferences.slugMap.apply { put(absSlug, slug) }
return this.replace(slug, absSlug)
}
companion object {
private val CLEAN_DATE_REGEX = """(\d+)(st|nd|rd|th)""".toRegex()
private val OLD_FORMAT_MANGA_REGEX = """^/manga/(\d+-)?([^/]+)/?$""".toRegex()
private val OLD_FORMAT_CHAPTER_REGEX = """^/(\d+-)?[^/]*-chapter-\d+(-\d+)*/?$""".toRegex()
private const val PREF_SLUG_MAP = "pref_slug_map"
private const val PREF_DYNAMIC_URL = "pref_dynamic_url"
} }
} }

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.en.asurascans
import kotlinx.serialization.Serializable
@Serializable
class FiltersDto(
val genres: List<FilterItemDto>,
val statuses: List<FilterItemDto>,
val types: List<FilterItemDto>,
)
@Serializable
class FilterItemDto(
val id: Int,
val name: String,
)

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.extension.en.asurascans
import eu.kanade.tachiyomi.source.model.Filter
class Genre(title: String, val id: Int) : Filter.CheckBox(title)
class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
class StatusFilter(title: String, statuses: List<Pair<String, String>>) : UriPartFilter(title, statuses)
class TypeFilter(title: String, types: List<Pair<String, String>>) : UriPartFilter(title, types)
class OrderFilter(title: String, orders: List<Pair<String, String>>) : UriPartFilter(title, orders)
open class UriPartFilter(displayName: String, val vals: List<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}