MangaHub: Update DTO, update token refresh logic (#13033)

* MangaHub: Update DTO, update token refresh logic

* ktLint
This commit is contained in:
Vetle Ledaal 2022-08-16 23:16:10 +02:00 committed by GitHub
parent d47b7684ce
commit b193ecceb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 49 additions and 86 deletions

View File

@ -17,6 +17,7 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Cookie
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
@ -26,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
@ -57,90 +60,48 @@ abstract class MangaHub(
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response { private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
parseApiKey(originalRequest).let { cdnApiKey = it }
val request = if (originalRequest.url.toString() == "$cdnApiUrl/graphql") { val cookie = client.cookieJar
val newRequest = originalRequest.newBuilder() .loadForRequest(baseUrl.toHttpUrl())
.header("x-mhub-access", cdnApiKey!!) .firstOrNull { it.name == "mhub_access" }
.build()
newRequest val request =
} else { if (originalRequest.url.toString() == "$cdnApiUrl/graphql" && cookie != null) {
originalRequest originalRequest.newBuilder()
} .header("x-mhub-access", cookie.value)
.build()
val originalResponse = chain.proceed(request) } else {
parseApiKey(originalResponse).let { cdnApiKey = it } originalRequest
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 { return chain.proceed(request)
originalResponse
}
} }
private var cdnApiKey: String? = null private fun refreshApiKey(chapter: SChapter) {
get() { val now = Calendar.getInstance().time.time
if (field == null) val url = "$baseUrl${chapter.url}".toHttpUrl()
field = fetchCdnApiKey(null) val number = chapter.url
return field .substringAfter("chapter-")
} .substringBefore("/")
set(value) {
if (value == null) return
field = value
}
private fun fetchCdnApiKey(slug: String?): String? { val recently = "{\"${now - (0..120).random()}\":{\"mangaID\":${(1..42000).random()},\"number\":$number}}"
val request = if (slug == null) {
GET("$baseUrl/?reloadKey=1", headers)
} else {
GET("$baseUrl/manga/$slug?reloadKey=1", headers)
}
// Bypass rate limit for this request client.cookieJar.saveFromResponse(
val response = super.client.newCall(request).execute() url,
return parseApiKey(response) 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()
} }
private fun parseApiKey(request: Request): String? =
request.header("Cookie")
?.split("; ")
?.mapNotNull { kv ->
val (k, v) = kv.split('=')
if (k == "mhub_access") v else null
}?.firstOrNull()
private fun parseApiKey(response: Response): String? =
response.headers("Set-Cookie")
.mapNotNull { kv ->
val (k, v) = kv.split("; ").first().split('=')
if (k == "mhub_access") v else null
}.firstOrNull()
override fun headersBuilder(): Headers.Builder { override fun headersBuilder(): Headers.Builder {
val defaultUserAgent = super.headersBuilder()["User-Agent"] val defaultUserAgent = super.headersBuilder()["User-Agent"]
@ -372,16 +333,22 @@ abstract class MangaHub(
return POST("$cdnApiUrl/graphql", jsonHeaders, body) return POST("$cdnApiUrl/graphql", jsonHeaders, body)
} }
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
super.fetchPageList(chapter)
.doOnError { refreshApiKey(chapter) }
.retry(1)
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val cdn = "$cdnImgUrl/file/imghub" val cdn = "$cdnImgUrl/file/imghub"
val chapterObject = json val chapterObject = json
.decodeFromString<GraphQLDataDto<ChapterDto>>(response.body!!.string()) .decodeFromString<GraphQLDataDto<ChapterDto>>(response.body!!.string())
if (chapterObject.errors != null) { if (chapterObject.data?.chapter == null) {
val errors = chapterObject.errors.joinToString("\n") { it.message } if (chapterObject.errors != null) {
throw Exception(errors) val errors = chapterObject.errors.joinToString("\n") { it.message }
} else if (chapterObject.data.chapter == null) { throw Exception(errors)
throw Exception("Unknown error while fetching pages") }
throw Exception("Unknown error while processing pages")
} }
val pagesObject = json val pagesObject = json
@ -503,17 +470,13 @@ 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 errors: List<ErrorDto>? = null,
val data: T val data: T? = null
) )
@Serializable @Serializable

View File

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