MangaHub: bypass API limits (#13012)

This commit is contained in:
Vetle Ledaal 2022-08-16 11:24:13 +00:00 committed by GitHub
parent 45e9ef7eab
commit 23d8aa7895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 81 additions and 16 deletions

View File

@ -56,42 +56,90 @@ abstract class MangaHub(
} }
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response { private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request().also { request -> val originalRequest = chain.request()
parseApiKey(request).let { if (it.isNotEmpty()) cdnApiKey = it } parseApiKey(originalRequest).let { cdnApiKey = it }
}
if (request.url.toString() == "$cdnApiUrl/graphql" && cdnApiKey!!.isNotEmpty()) { val request = if (originalRequest.url.toString() == "$cdnApiUrl/graphql") {
val newRequest = request.newBuilder() val newRequest = originalRequest.newBuilder()
.addHeader("x-mhub-access", cdnApiKey!!) .header("x-mhub-access", cdnApiKey!!)
.build() .build()
return chain.proceed(newRequest) newRequest
} else {
originalRequest
} }
return chain.proceed(request).also { response -> val originalResponse = chain.proceed(request)
parseApiKey(response).let { if (it.isNotEmpty()) cdnApiKey = it } parseApiKey(originalResponse).let { cdnApiKey = it }
return if (request.url.toString() == "$cdnApiUrl/graphql") {
var newResponse: Response? = null
runCatching {
val responseData = json
.decodeFromString<GraphQLDataDto<ChapterDto>>(originalResponse.peekBody(1 * 1024 * 1024).string())
if (
responseData.errors?.isNotEmpty() == true ||
responseData.data.chapter == null
) {
val buffer = okio.Buffer()
request.body!!.writeTo(buffer)
val requestBody = buffer.readUtf8()
val slug = PATTERN_SLUG.find(requestBody)?.groupValues?.get(1)
cdnApiKey = fetchCdnApiKey(slug)
val newRequest = originalRequest.newBuilder()
.header("x-mhub-access", cdnApiKey!!)
.build()
// Bypass rate limit for this request
newResponse = super.client.newCall(newRequest).execute()
}
}
newResponse ?: originalResponse
} else {
originalResponse
} }
} }
private var cdnApiKey: String? = null private var cdnApiKey: String? = null
get() = field ?: client.newCall(GET(baseUrl, headers)).execute().let(::parseApiKey) get() {
if (field == null)
field = fetchCdnApiKey(null)
return field
}
set(value) {
if (value == null) return
field = value
}
private fun parseApiKey(request: Request): String = private fun fetchCdnApiKey(slug: String?): String? {
val request = if (slug == null) {
GET("$baseUrl/?reloadKey=1", headers)
} else {
GET("$baseUrl/manga/$slug?reloadKey=1", headers)
}
// Bypass rate limit for this request
val response = super.client.newCall(request).execute()
return parseApiKey(response)
}
private fun parseApiKey(request: Request): String? =
request.header("Cookie") request.header("Cookie")
?.split("; ") ?.split("; ")
?.mapNotNull { kv -> ?.mapNotNull { kv ->
val (k, v) = kv.split('=') val (k, v) = kv.split('=')
if (k == "mhub_access") v else null if (k == "mhub_access") v else null
}?.firstOrNull() }?.firstOrNull()
?: ""
private fun parseApiKey(response: Response): String = private fun parseApiKey(response: Response): String? =
response.headers("Set-Cookie") response.headers("Set-Cookie")
.mapNotNull { kv -> .mapNotNull { kv ->
val (k, v) = kv.split("; ").first().split('=') val (k, v) = kv.split("; ").first().split('=')
if (k == "mhub_access") v else null if (k == "mhub_access") v else null
}.firstOrNull() }.firstOrNull()
?: ""
override fun headersBuilder(): Headers.Builder { override fun headersBuilder(): Headers.Builder {
val defaultUserAgent = super.headersBuilder()["User-Agent"] val defaultUserAgent = super.headersBuilder()["User-Agent"]
@ -329,6 +377,13 @@ abstract class MangaHub(
val chapterObject = json val chapterObject = json
.decodeFromString<GraphQLDataDto<ChapterDto>>(response.body!!.string()) .decodeFromString<GraphQLDataDto<ChapterDto>>(response.body!!.string())
if (chapterObject.errors != null) {
val errors = chapterObject.errors.joinToString("\n") { it.message }
throw Exception(errors)
} else if (chapterObject.data.chapter == null) {
throw Exception("Unknown error while fetching pages")
}
val pagesObject = json val pagesObject = json
.decodeFromString<JsonObject>(chapterObject.data.chapter.pages) .decodeFromString<JsonObject>(chapterObject.data.chapter.pages)
val pages = pagesObject.values.map { it.jsonPrimitive.content } val pages = pagesObject.values.map { it.jsonPrimitive.content }
@ -448,17 +503,27 @@ abstract class MangaHub(
Genre("Yaoi", "yaoi"), Genre("Yaoi", "yaoi"),
Genre("Yuri", "yuri") Genre("Yuri", "yuri")
) )
companion object {
private val PATTERN_SLUG = ",slug:\\\\\"([^\\\\]+)\\\\\",".toRegex()
}
} }
// DTO // DTO
@Serializable @Serializable
data class GraphQLDataDto<T>( data class GraphQLDataDto<T>(
val errors: List<ErrorDto>? = null,
val data: T val data: T
) )
@Serializable
data class ErrorDto(
val message: String
)
@Serializable @Serializable
data class ChapterDto( data class ChapterDto(
val chapter: ChapterInnerDto val chapter: ChapterInnerDto?
) )
@Serializable @Serializable

View File

@ -9,7 +9,7 @@ class MangaHubGenerator : ThemeSourceGenerator {
override val themeClass = "MangaHub" override val themeClass = "MangaHub"
override val baseVersionCode: Int = 7 override val baseVersionCode: Int = 8
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"),