MangaHub: Add getting API Key back (#16392)

This commit is contained in:
Slowlife 2023-05-10 20:40:49 +07:00 committed by GitHub
parent 628a076e32
commit 169a8dfbe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 17 deletions

View File

@ -15,7 +15,10 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.Cookie
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -24,7 +27,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -39,8 +44,12 @@ abstract class MangaHub(
override val supportsLatest = true 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() override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(::uaIntercept) .addInterceptor(::uaIntercept)
.addInterceptor(::apiAuthInterceptor)
.rateLimit(1) .rateLimit(1)
.build() .build()
@ -56,9 +65,6 @@ abstract class MangaHub(
open val json: Json by injectLazy() 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 userAgent: String? = null
private var checkedUa = false private var checkedUa = false
@ -88,6 +94,63 @@ abstract class MangaHub(
return chain.proceed(chain.request()) 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 // popular
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/popular/page/$page", headers) return GET("$baseUrl/popular/page/$page", headers)
@ -148,16 +211,16 @@ abstract class MangaHub(
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
/* /*
* To remove duplicates we group by the thumbnail_url, which is * To remove duplicates we group by the thumbnail_url, which is
* common between duplicates. The duplicates have a suffix in the * common between duplicates. The duplicates have a suffix in the
* url "-by-{name}". Here we select the shortest url, to avoid * url "-by-{name}". Here we select the shortest url, to avoid
* removing manga that has "by" in the title already. * removing manga that has "by" in the title already.
* Example: * Example:
* /manga/tales-of-demons-and-gods (kept) * /manga/tales-of-demons-and-gods (kept)
* /manga/tales-of-demons-and-gods-by-mad-snail (removed) * /manga/tales-of-demons-and-gods-by-mad-snail (removed)
* /manga/leveling-up-by-only-eating (kept) * /manga/leveling-up-by-only-eating (kept)
*/ */
val mangas = document.select(searchMangaSelector()).map { element -> val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element) searchMangaFromElement(element)
}.groupBy { it.thumbnail_url }.mapValues { (_, values) -> }.groupBy { it.thumbnail_url }.mapValues { (_, values) ->
@ -301,23 +364,55 @@ abstract class MangaHub(
.toRequestBody() .toRequestBody()
val newHeaders = headersBuilder() val newHeaders = headersBuilder()
.set("Accept", "application/json")
.set("Content-Type", "application/json") .set("Content-Type", "application/json")
.set("Origin", baseUrl) .set("Origin", baseUrl)
.set("Sec-Fetch-Dest", "empty")
.set("Sec-Fetch-Mode", "cors")
.set("Sec-Fetch-Site", "cross-site")
.removeAll("Upgrade-Insecure-Requests")
.build() .build()
return POST("$baseApiUrl/graphql", newHeaders, body) 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(document: Document): List<Page> = throw UnsupportedOperationException("Not used")
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val pagesString = json.decodeFromString<ApiChapterPagesResponse>(response.body.string()).data.chapter.pages val chapterObject = json.decodeFromString<ApiChapterPagesResponse>(response.body.string())
val pages = json.decodeFromString<ApiChapterPages>(pagesString)
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 -> return pages.i.mapIndexed { i, page ->
Page(i, "", "$baseCdnUrl/${pages.p}$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") override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
// filters // filters

View File

@ -9,7 +9,7 @@ class MangaHubGenerator : ThemeSourceGenerator {
override val themeClass = "MangaHub" override val themeClass = "MangaHub"
override val baseVersionCode: Int = 18 override val baseVersionCode: Int = 19
override val sources = listOf( override val sources = listOf(
// SingleLang("1Manga.co", "https://1manga.co", "en", isNsfw = true, className = "OneMangaCo"), // SingleLang("1Manga.co", "https://1manga.co", "en", isNsfw = true, className = "OneMangaCo"),

View File

@ -14,9 +14,15 @@ val PAGES_QUERY = buildQuery {
""".trimIndent() """.trimIndent()
} }
@Serializable
data class ApiErrorMessages(
val message: String,
)
@Serializable @Serializable
data class ApiChapterPagesResponse( data class ApiChapterPagesResponse(
val data: ApiChapterData, val data: ApiChapterData?,
val errors: List<ApiErrorMessages>?,
) )
@Serializable @Serializable