MangaHub: Add getting API Key back (#16392)
This commit is contained in:
parent
628a076e32
commit
169a8dfbe4
|
@ -15,7 +15,10 @@ import kotlinx.serialization.decodeFromString
|
|||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -24,7 +27,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
|
@ -39,8 +44,12 @@ abstract class MangaHub(
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
private var baseApiUrl = "https://api.mghubcdn.com"
|
||||
private var baseCdnUrl = "https://imgx.mghubcdn.com"
|
||||
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.addInterceptor(::uaIntercept)
|
||||
.addInterceptor(::apiAuthInterceptor)
|
||||
.rateLimit(1)
|
||||
.build()
|
||||
|
||||
|
@ -56,9 +65,6 @@ abstract class MangaHub(
|
|||
|
||||
open val json: Json by injectLazy()
|
||||
|
||||
private var baseApiUrl = "https://api.mghubcdn.com"
|
||||
private var baseCdnUrl = "https://imgx.mghubcdn.com"
|
||||
|
||||
private var userAgent: String? = null
|
||||
private var checkedUa = false
|
||||
|
||||
|
@ -88,6 +94,63 @@ abstract class MangaHub(
|
|||
return chain.proceed(chain.request())
|
||||
}
|
||||
|
||||
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val cookie = client.cookieJar
|
||||
.loadForRequest(baseUrl.toHttpUrl())
|
||||
.firstOrNull { it.name == "mhub_access" }
|
||||
|
||||
val request =
|
||||
if (originalRequest.url.toString() == "$baseApiUrl/graphql" && cookie != null) {
|
||||
originalRequest.newBuilder()
|
||||
.header("x-mhub-access", cookie.value)
|
||||
.build()
|
||||
} else {
|
||||
originalRequest
|
||||
}
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun refreshApiKey(chapter: SChapter) {
|
||||
val now = Calendar.getInstance().time.time
|
||||
|
||||
val slug = "$baseUrl${chapter.url}"
|
||||
.toHttpUrlOrNull()
|
||||
?.pathSegments
|
||||
?.get(1)
|
||||
|
||||
val url = if (slug != null) {
|
||||
"$baseUrl/manga/$slug".toHttpUrl()
|
||||
} else {
|
||||
baseUrl.toHttpUrl()
|
||||
}
|
||||
|
||||
// Set required cookie (for cache busting?)
|
||||
val recently = buildJsonObject {
|
||||
putJsonObject((now - (0..3600).random()).toString()) {
|
||||
put("mangaID", (1..42_000).random())
|
||||
put("number", (1..20).random())
|
||||
}
|
||||
}.toString()
|
||||
|
||||
client.cookieJar.saveFromResponse(
|
||||
url,
|
||||
listOf(
|
||||
Cookie.Builder()
|
||||
.domain(url.host)
|
||||
.name("recently")
|
||||
.value(URLEncoder.encode(recently, "utf-8"))
|
||||
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
|
||||
val request = GET("$url?reloadKey=1", headers)
|
||||
client.newCall(request).execute()
|
||||
}
|
||||
|
||||
// popular
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/popular/page/$page", headers)
|
||||
|
@ -148,16 +211,16 @@ abstract class MangaHub(
|
|||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
/*
|
||||
* To remove duplicates we group by the thumbnail_url, which is
|
||||
* common between duplicates. The duplicates have a suffix in the
|
||||
* url "-by-{name}". Here we select the shortest url, to avoid
|
||||
* removing manga that has "by" in the title already.
|
||||
* Example:
|
||||
* /manga/tales-of-demons-and-gods (kept)
|
||||
* /manga/tales-of-demons-and-gods-by-mad-snail (removed)
|
||||
* /manga/leveling-up-by-only-eating (kept)
|
||||
*/
|
||||
/*
|
||||
* To remove duplicates we group by the thumbnail_url, which is
|
||||
* common between duplicates. The duplicates have a suffix in the
|
||||
* url "-by-{name}". Here we select the shortest url, to avoid
|
||||
* removing manga that has "by" in the title already.
|
||||
* Example:
|
||||
* /manga/tales-of-demons-and-gods (kept)
|
||||
* /manga/tales-of-demons-and-gods-by-mad-snail (removed)
|
||||
* /manga/leveling-up-by-only-eating (kept)
|
||||
*/
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}.groupBy { it.thumbnail_url }.mapValues { (_, values) ->
|
||||
|
@ -301,23 +364,55 @@ abstract class MangaHub(
|
|||
.toRequestBody()
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Accept", "application/json")
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Origin", baseUrl)
|
||||
.set("Sec-Fetch-Dest", "empty")
|
||||
.set("Sec-Fetch-Mode", "cors")
|
||||
.set("Sec-Fetch-Site", "cross-site")
|
||||
.removeAll("Upgrade-Insecure-Requests")
|
||||
.build()
|
||||
|
||||
return POST("$baseApiUrl/graphql", newHeaders, body)
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||
super.fetchPageList(chapter)
|
||||
.doOnError { refreshApiKey(chapter) }
|
||||
.retry(1)
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException("Not used")
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pagesString = json.decodeFromString<ApiChapterPagesResponse>(response.body.string()).data.chapter.pages
|
||||
val pages = json.decodeFromString<ApiChapterPages>(pagesString)
|
||||
val chapterObject = json.decodeFromString<ApiChapterPagesResponse>(response.body.string())
|
||||
|
||||
if (chapterObject.data?.chapter == null) {
|
||||
if (chapterObject.errors != null) {
|
||||
val errors = chapterObject.errors.joinToString("\n") { it.message }
|
||||
throw Exception(errors)
|
||||
}
|
||||
throw Exception("Unknown error while processing pages")
|
||||
}
|
||||
|
||||
val pages = json.decodeFromString<ApiChapterPages>(chapterObject.data.chapter.pages)
|
||||
|
||||
return pages.i.mapIndexed { i, page ->
|
||||
Page(i, "", "$baseCdnUrl/${pages.p}$page")
|
||||
}
|
||||
}
|
||||
|
||||
// Image
|
||||
override fun imageUrlRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Accept", "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
.set("Sec-Fetch-Dest", "image")
|
||||
.set("Sec-Fetch-Mode", "no-cors")
|
||||
.set("Sec-Fetch-Site", "cross-site")
|
||||
.removeAll("Upgrade-Insecure-Requests")
|
||||
.build()
|
||||
|
||||
return GET(page.url, newHeaders)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// filters
|
||||
|
|
|
@ -9,7 +9,7 @@ class MangaHubGenerator : ThemeSourceGenerator {
|
|||
|
||||
override val themeClass = "MangaHub"
|
||||
|
||||
override val baseVersionCode: Int = 18
|
||||
override val baseVersionCode: Int = 19
|
||||
|
||||
override val sources = listOf(
|
||||
// SingleLang("1Manga.co", "https://1manga.co", "en", isNsfw = true, className = "OneMangaCo"),
|
||||
|
|
|
@ -14,9 +14,15 @@ val PAGES_QUERY = buildQuery {
|
|||
""".trimIndent()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiErrorMessages(
|
||||
val message: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiChapterPagesResponse(
|
||||
val data: ApiChapterData,
|
||||
val data: ApiChapterData?,
|
||||
val errors: List<ApiErrorMessages>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
Loading…
Reference in New Issue