Fix InfinityScans (#6269)

This commit is contained in:
Michał Marszałek 2024-11-28 14:39:51 +01:00 committed by Draff
parent 5911343a9a
commit bcf51e8138
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
4 changed files with 121 additions and 125 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'InfinityScans' extName = 'InfinityScans'
extClass = '.InfinityScans' extClass = '.InfinityScans'
extVersionCode = 4 extVersionCode = 5
isNsfw = true isNsfw = true
} }

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.en.infinityscans
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.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -13,15 +14,12 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MultipartBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class InfinityScans : HttpSource() { class InfinityScans : HttpSource() {
@ -29,7 +27,7 @@ class InfinityScans : HttpSource() {
override val name = "InfinityScans" override val name = "InfinityScans"
override val baseUrl = "https://infinityscans.net" override val baseUrl = "https://infinityscans.net"
private val cdnHost = "cdn.infinityscans.xyz" private val cdnHost = "cdn.infinityscans.net"
override val lang = "en" override val lang = "en"
@ -49,59 +47,106 @@ class InfinityScans : HttpSource() {
add("Sec-Fetch-Dest", "empty") add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors") add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "same-origin") add("Sec-Fetch-Site", "same-origin")
add("X-Requested-With", "XMLHttpRequest") add("X-requested-with", "XMLHttpRequest")
}.build() }.build()
private val json: Json by injectLazy() private val json: Json by injectLazy()
// Popular // Popular
override fun popularMangaRequest(page: Int): Request = override fun popularMangaRequest(page: Int): Request = fetchJson("api/ranking")
searchMangaRequest(page, "", FilterList(SortFilter("popularity")))
override fun popularMangaParse(response: Response): MangasPage = override fun popularMangaParse(response: Response): MangasPage {
searchMangaParse(response) val data = response.parseAs<ResponseDto<RankingResultDto>>().result
val entries = data.weekly
.map { it.toSManga(cdnHost) }
return MangasPage(entries, false)
}
// Latest // Latest
override fun latestUpdatesRequest(page: Int): Request = override fun latestUpdatesRequest(page: Int): Request = fetchJson("api/comics")
searchMangaRequest(page, "", FilterList(SortFilter("latest")))
override fun latestUpdatesParse(response: Response): MangasPage = override fun latestUpdatesParse(response: Response): MangasPage {
searchMangaParse(response) val data = response.parseAs<ResponseDto<SearchResultDto>>().result
runCatching { updateFilters(data) }
val entries = data.titles.sortedByDescending { it.updated }
.map { it.toSManga(cdnHost) }
return MangasPage(entries, false)
}
// Search // Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun fetchSearchManga(
val url = baseUrl.toHttpUrl().newBuilder() page: Int,
.addPathSegments("ajax/comics") query: String,
.addQueryParameter("page", page.toString()) filters: FilterList,
): Observable<MangasPage> {
return client.newCall(fetchJson("api/comics"))
.asObservableSuccess()
.map { response ->
val data = response.parseAs<ResponseDto<SearchResultDto>>().result
runCatching { updateFilters(data) }
var titles = data.titles
filters.forEach { filter -> if (query.isNotBlank()) {
when (filter) { titles = titles.filter { it.title.contains(query, ignoreCase = true) }
is SortFilter -> {
url.addQueryParameter("sort", filter.selected)
} }
is GenreFilter -> {
filter.checked?.also { filters.forEach { filter ->
url.addQueryParameter("genre", it.joinToString("|")) when (filter) {
is SortFilter -> {
when (filter.selected) {
"title" -> {
titles = titles.sortedBy { it.title }
}
"popularity" -> {
titles = titles.sortedByDescending { it.all_views }
}
"latest" -> {
titles = titles.sortedByDescending { it.updated }
}
}
}
is GenreFilter -> {
filter.checked?.also {
titles = titles.filter { it.genres?.split(",")?.any { genre -> genre in filter.checked!! } ?: true }
}
}
is AuthorFilter -> {
filter.checked?.also {
titles = titles.filter { it.authors?.split(",")?.any { author -> author in filter.checked!! } ?: true }
}
}
is StatusFilter -> {
filter.checked?.also {
titles = titles.filter { filter.checked!!.any { status -> status == it.status } }
}
}
else -> { /* Do Nothing */
}
} }
} }
is AuthorFilter -> {
filter.checked?.also { val entries = titles.map { it.toSManga(cdnHost) }
url.addQueryParameter("author", it.joinToString("|"))
} MangasPage(entries, false)
}
is StatusFilter -> {
filter.checked?.also {
url.addQueryParameter("status", it.joinToString("|"))
}
}
else -> { /* Do Nothing */ }
} }
} }
if (query.isNotBlank()) url.addQueryParameter("title", query) override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
private fun fetchJson(api: String): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegments(api)
val searchHeaders = apiHeaders.newBuilder().apply { val searchHeaders = apiHeaders.newBuilder().apply {
set("Referer", url.build().newBuilder().removePathSegment(0).build().toString()) set("Referer", url.build().newBuilder().removePathSegment(0).build().toString())
@ -110,17 +155,6 @@ class InfinityScans : HttpSource() {
return GET(url.build(), searchHeaders) return GET(url.build(), searchHeaders)
} }
override fun searchMangaParse(response: Response): MangasPage {
val page = response.request.url.queryParameter("page")!!.toInt()
val data = response.parseAs<ResponseDto<SearchResultDto>>().result
runCatching { updateFilters(data) }
val entries = data.comics
.map { it.toSManga(cdnHost) }
return MangasPage(entries, page < data.pages)
}
// Filters // Filters
private fun updateFilters(data: SearchResultDto) { private fun updateFilters(data: SearchResultDto) {
@ -165,20 +199,20 @@ class InfinityScans : HttpSource() {
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val document = response.use { it.asJsoup() } val document = response.use { it.asJsoup() }
val desc = document.select("div.s1:has(>h2:contains(Summary)) p") val desc = document.select("div:has(>h4:contains(Summary)) p")
.text() .text()
.split("</br>") .split("</br>")
.joinToString("\n", transform = String::trim) .joinToString("\n", transform = String::trim)
.trim() .trim()
return SManga.create().apply { return SManga.create().apply {
document.selectFirst("div.info")!!.also { details -> document.selectFirst("div:has(>span:contains(Rank:))")!!.parent()!!.also { details ->
description = desc description = desc
author = details.getLinks("Authors") author = details.getLinks("Authors")
genre = details.getLinks("Genres") genre = details.getLinks("Genres")
status = details.getInfo("Status").parseStatus() status = details.getInfo("Status").parseStatus()
details.getInfo("Alternative Title")?.let { details.getInfo("Alternative Titles")?.let {
description = "$desc\n\nAlternative Title: $it" description = "$desc\n\nAlternative Title: $it"
} }
} }
@ -196,10 +230,10 @@ class InfinityScans : HttpSource() {
} }
private fun Element.getInfo(name: String): String? = private fun Element.getInfo(name: String): String? =
selectFirst("div:has(>b:matches($name:))")?.ownText() selectFirst("div:has(>span:matches($name:))")?.ownText()
private fun Element.getLinks(name: String): String? = private fun Element.getLinks(name: String): String? =
select("div:has(>b:matches($name:)) a") select("div:has(>span:matches($name:)) a")
.joinToString(", ", transform = Element::text).trim() .joinToString(", ", transform = Element::text).trim()
.takeIf { it.isNotBlank() } .takeIf { it.isNotBlank() }
@ -210,23 +244,17 @@ class InfinityScans : HttpSource() {
val slug = url.pathSegments.take(3).joinToString("/", prefix = "/") val slug = url.pathSegments.take(3).joinToString("/", prefix = "/")
// Create POST request // Create POST request
val id = url.pathSegments[1]
val form = FormBody.Builder().apply {
add("comic_id", id)
}.build()
val chapterHeaders = apiHeaders.newBuilder().apply { val chapterHeaders = apiHeaders.newBuilder().apply {
add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") add("content-length", "0")
add("Host", baseUrl.toHttpUrl().host)
add("Origin", baseUrl) add("Origin", baseUrl)
set("Referer", url.toString()) set("Referer", url.toString())
}.build() }.build()
val chapterListData = client.newCall( val chapterListData = client.newCall(
POST("$baseUrl/ajax/chapters", chapterHeaders, form), POST(url.toString(), chapterHeaders),
).execute().parseAs<ResponseDto<ChapterDataDto>>() ).execute().parseAs<ResponseDto<List<ChapterEntryDto>>>()
return chapterListData.result.chapters.map { return chapterListData.result.map {
it.toSChapter(slug) it.toSChapter(slug)
} }
} }
@ -235,38 +263,19 @@ class InfinityScans : HttpSource() {
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val url = response.request.url val url = response.request.url
val boundary = buildString {
append((1..9).random())
repeat(28) {
append((0..9).random())
}
}
// Create POST request // Create POST request
val form = MultipartBody.Builder("-----------------------------$boundary").apply {
setType(MultipartBody.FORM)
addPart(
Headers.headersOf("Content-Disposition", "form-data; name=\"comic_id\""),
url.pathSegments[1].toRequestBody(null),
)
addPart(
Headers.headersOf("Content-Disposition", "form-data; name=\"chapter_id\""),
url.pathSegments[4].toRequestBody(null),
)
}.build()
val pageListHeaders = apiHeaders.newBuilder().apply { val pageListHeaders = apiHeaders.newBuilder().apply {
add("Host", url.host) add("content-length", "0")
add("Origin", baseUrl) add("Origin", baseUrl)
set("Referer", url.toString()) set("Referer", url.toString())
}.build() }.build()
val pageListData = client.newCall( val pageListData = client.newCall(
POST("$baseUrl/ajax/images", pageListHeaders, form), POST(url.toString(), pageListHeaders),
).execute().parseAs<ResponseDto<PageDataDto>>() ).execute().parseAs<ResponseDto<List<PageEntryDto>>>()
return pageListData.result.images.mapIndexed { index, p -> return pageListData.result.mapIndexed { index, p ->
Page(index, url.toString(), p.link) Page(index, url.toString(), p.link)
} }
} }
@ -277,7 +286,7 @@ class InfinityScans : HttpSource() {
override fun imageRequest(page: Page): Request { override fun imageRequest(page: Page): Request {
val pageHeaders = headersBuilder().apply { val pageHeaders = headersBuilder().apply {
add("Accept", "image/avif,image/webp,*/*") add("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*")
add("Host", page.imageUrl!!.toHttpUrl().host) add("Host", page.imageUrl!!.toHttpUrl().host)
}.build() }.build()

View File

@ -3,82 +3,69 @@ package eu.kanade.tachiyomi.extension.en.infinityscans
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 kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable @Serializable
class ResponseDto<T>(val result: T) class ResponseDto<T>(val result: T)
@Serializable @Serializable
data class SearchResultDto( data class SearchResultDto(
val pages: Int, val titles: List<SearchEntryDto>,
val comics: List<SearchEntryDto>,
val genres: List<FilterDto>? = null, val genres: List<FilterDto>? = null,
val authors: List<FilterDto>? = null, val authors: List<FilterDto>? = null,
val statuses: List<String>? = null, val statuses: List<String>? = null,
) )
@Serializable
data class RankingResultDto(
val weekly: List<SearchEntryDto>,
val monthly: List<SearchEntryDto>,
val all: List<SearchEntryDto>,
)
@Serializable @Serializable
data class SearchEntryDto( data class SearchEntryDto(
val id: Int, val id: Int,
val title: String, val title: String,
val slug: String, val slug: String,
val cover: String, val cover: String,
val updated: String? = null, val authors: String?,
val created: String? = null, val genres: String?,
val all_views: Int? = null,
val status: String?,
val updated: Int? = null,
) { ) {
fun toSManga(cdnHost: String) = SManga.create().apply { fun toSManga(cdnHost: String) = SManga.create().apply {
title = this@SearchEntryDto.title title = this@SearchEntryDto.title
thumbnail_url = "https://$cdnHost/$id/$cover?v=${getImageParameter()}" thumbnail_url = "https://$cdnHost/$id/$cover?_=${getImageParameter()}"
url = "/comic/$id/$slug" url = "/comic/$id/$slug"
} }
private fun getImageParameter(): Long { private fun getImageParameter(): Long {
val date = updated?.let { parseDate(it, DATE_FORMATTER) } return updated?.toLong() ?: 0L
?: created?.let { parseDate(it, DATE_FORMATTER) }
?: 0L
return date / 1000L
} }
} }
@Serializable @Serializable
data class FilterDto(val id: Int, val title: String) data class FilterDto(val id: Int, val title: String)
@Serializable
data class ChapterDataDto(val chapters: List<ChapterEntryDto>)
@Serializable @Serializable
data class ChapterEntryDto( data class ChapterEntryDto(
val id: Int, val id: Int,
val title: String, val title: String,
val sequence: Int, val sequence: Int,
val date: String, val date: Int,
) { ) {
fun toSChapter(slug: String) = SChapter.create().apply { fun toSChapter(slug: String) = SChapter.create().apply {
name = title name = title
// Things like prologues mess up the sequence number // Things like prologues mess up the sequence number
chapter_number = title.substringAfter("hapter ").toFloatOrNull() ?: sequence.toFloat() chapter_number = title.substringAfter("hapter ").toFloatOrNull() ?: sequence.toFloat()
date_upload = parseDate(date, CHAPTER_FORMATTER) date_upload = date.toLong() * 1000
url = "$slug/chapter/$id" url = "$slug/chapter/$id"
} }
} }
@Serializable @Serializable
data class PageDataDto(val images: List<PageEntryDto>) data class PageEntryDto(
val link: String,
@Serializable )
data class PageEntryDto(val link: String)
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
}
private val CHAPTER_FORMATTER by lazy {
SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault())
}
private fun parseDate(dateStr: String, formatter: SimpleDateFormat): Long {
return runCatching { formatter.parse(dateStr)?.time }
.getOrNull() ?: 0L
}

View File

@ -23,14 +23,14 @@ class WebviewInterceptor(private val baseUrl: String) : Interceptor {
val request = chain.request() val request = chain.request()
val origRes = chain.proceed(request) val origRes = chain.proceed(request)
if (origRes.code != 401) return origRes if (origRes.code != 400) return origRes
origRes.close() origRes.close()
resolveInWebview() resolveInWebview()
// If webview failed // If webview failed
val response = chain.proceed(request) val response = chain.proceed(request)
if (response.code == 401) { if (response.code == 400) {
response.close() response.close()
throw IOException("Solve Captcha in WebView") throw IOException("Solve Captcha in WebView")
} }
@ -55,7 +55,7 @@ class WebviewInterceptor(private val baseUrl: String) : Interceptor {
webview.webViewClient = object : WebViewClient() { webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
if (request?.url.toString().contains("$baseUrl/ajax/turnstile")) { if (request?.url.toString().contains("$baseUrl/api/verification")) {
hasSetCookies = true hasSetCookies = true
} else if (request?.url.toString().contains(baseUrl) && hasSetCookies) { } else if (request?.url.toString().contains(baseUrl) && hasSetCookies) {
latch.countDown() latch.countDown()