mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-26 04:03:01 +01:00
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:
parent
86f539c3e7
commit
afeea1ad44
@ -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"
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
)
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user