6563490513
Includes side effects: - No longer need to restart app for user agent string change to take effect - parseAs extension function requires a Json instance in the calling context, which doesn't necessarily need to be the default one provided by Injekt (cherry picked from commit 93523ef50b80ef294866bfb0da54e236cdf2d9f6) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt # app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt # core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt # core/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt # domain/build.gradle.kts # source-api/build.gradle.kts
244 lines
8.2 KiB
Kotlin
244 lines
8.2 KiB
Kotlin
package exh.md.handlers
|
|
|
|
import eu.kanade.tachiyomi.network.POST
|
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
import eu.kanade.tachiyomi.network.parseAs
|
|
import eu.kanade.tachiyomi.source.model.Page
|
|
import eu.kanade.tachiyomi.source.model.SChapter
|
|
import exh.log.xLogD
|
|
import kotlinx.serialization.SerialName
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.json.Json
|
|
import kotlinx.serialization.json.add
|
|
import kotlinx.serialization.json.buildJsonArray
|
|
import kotlinx.serialization.json.buildJsonObject
|
|
import kotlinx.serialization.json.put
|
|
import okhttp3.Headers
|
|
import okhttp3.MediaType.Companion.toMediaType
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
|
import okhttp3.Response
|
|
import rx.Observable
|
|
import uy.kohesive.injekt.injectLazy
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
class BilibiliHandler(currentClient: OkHttpClient) {
|
|
val baseUrl = "https://www.bilibilicomics.com"
|
|
val headers = Headers.Builder()
|
|
.add("Accept", ACCEPT_JSON)
|
|
.add("Origin", baseUrl)
|
|
.add("Referer", "$baseUrl/")
|
|
.build()
|
|
|
|
val client: OkHttpClient = currentClient.newBuilder()
|
|
.rateLimit(1, 1, TimeUnit.SECONDS)
|
|
.build()
|
|
|
|
val json by injectLazy<Json>()
|
|
|
|
suspend fun fetchPageList(externalUrl: String, chapterNumber: String): List<Page> {
|
|
// Sometimes the urls direct it to the manga page instead, so we try to find the correct chapter
|
|
// Though these seem to be older chapters, so maybe remove this later
|
|
val chapterUrl = if (externalUrl.contains("mc\\d*/\\d*".toRegex())) {
|
|
getChapterUrl(externalUrl)
|
|
} else {
|
|
val mangaUrl = getMangaUrl(externalUrl)
|
|
val chapters = getChapterList(mangaUrl)
|
|
val chapter = chapters
|
|
.find { it.chapter_number == chapterNumber.toFloatOrNull() }
|
|
?: throw Exception("Unknown chapter $chapterNumber")
|
|
chapter.url
|
|
}
|
|
|
|
return fetchPageList(chapterUrl)
|
|
}
|
|
|
|
private fun getMangaUrl(externalUrl: String): String {
|
|
xLogD(externalUrl)
|
|
val comicId = externalUrl
|
|
.substringAfter("/mc")
|
|
.substringBefore('?')
|
|
.toInt()
|
|
|
|
return "/detail/mc$comicId"
|
|
}
|
|
|
|
private fun getChapterUrl(externalUrl: String): String {
|
|
val comicId = externalUrl.substringAfterLast("/mc")
|
|
.substringBefore('/')
|
|
.toInt()
|
|
val episodeId = externalUrl.substringAfterLast('/')
|
|
.substringBefore('?')
|
|
.toInt()
|
|
return "/mc$comicId/$episodeId"
|
|
}
|
|
|
|
private fun mangaDetailsApiRequest(mangaUrl: String): Request {
|
|
val comicId = mangaUrl.substringAfterLast("/mc").toInt()
|
|
|
|
val jsonPayload = buildJsonObject { put("comic_id", comicId) }
|
|
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
|
|
val newHeaders = headers.newBuilder()
|
|
.add("Content-Length", requestBody.contentLength().toString())
|
|
.add("Content-Type", requestBody.contentType().toString())
|
|
.set("Referer", baseUrl + mangaUrl)
|
|
.build()
|
|
|
|
return POST(
|
|
"$baseUrl/$BASE_API_ENDPOINT/ComicDetail?device=pc&platform=web",
|
|
headers = newHeaders,
|
|
body = requestBody,
|
|
)
|
|
}
|
|
|
|
suspend fun getChapterList(mangaUrl: String): List<SChapter> {
|
|
val response = client.newCall(mangaDetailsApiRequest(mangaUrl)).awaitSuccess()
|
|
return chapterListParse(response)
|
|
}
|
|
|
|
fun chapterListParse(response: Response): List<SChapter> {
|
|
val result = with(json) { response.parseAs<BilibiliResultDto<BilibiliComicDto>>() }
|
|
|
|
if (result.code != 0) {
|
|
return emptyList()
|
|
}
|
|
|
|
return result.data!!.episodeList
|
|
.filter { episode -> episode.isLocked.not() }
|
|
.map { ep -> chapterFromObject(ep, result.data.id) }
|
|
}
|
|
|
|
private fun chapterFromObject(episode: BilibiliEpisodeDto, comicId: Int): SChapter = SChapter(
|
|
url = "/mc$comicId/${episode.id}",
|
|
name = "Ep. " + episode.order.toString().removeSuffix(".0") + " - " + episode.title,
|
|
chapter_number = episode.order,
|
|
)
|
|
|
|
private suspend fun fetchPageList(chapterUrl: String): List<Page> {
|
|
val response = client.newCall(pageListRequest(chapterUrl)).awaitSuccess()
|
|
return pageListParse(response)
|
|
}
|
|
|
|
private fun pageListRequest(chapterUrl: String): Request {
|
|
val chapterId = chapterUrl.substringAfterLast("/").toInt()
|
|
|
|
val jsonPayload = buildJsonObject { put("ep_id", chapterId) }
|
|
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
|
|
val newHeaders = headers
|
|
.newBuilder()
|
|
.add("Content-Length", requestBody.contentLength().toString())
|
|
.add("Content-Type", requestBody.contentType().toString())
|
|
.set("Referer", baseUrl + chapterUrl)
|
|
.build()
|
|
|
|
return POST(
|
|
"$baseUrl/$BASE_API_ENDPOINT/GetImageIndex?device=pc&platform=web",
|
|
headers = newHeaders,
|
|
body = requestBody,
|
|
)
|
|
}
|
|
|
|
private fun pageListParse(response: Response): List<Page> {
|
|
val result = with(json) { response.parseAs<BilibiliResultDto<BilibiliReader>>() }
|
|
|
|
if (result.code != 0) {
|
|
return emptyList()
|
|
}
|
|
|
|
return result.data!!.images
|
|
.mapIndexed { i, page -> Page(i, page.path, "") }
|
|
}
|
|
|
|
fun fetchImageUrl(page: Page): Observable<String> {
|
|
return client.newCall(imageUrlRequest(page))
|
|
.asObservableSuccess()
|
|
.map {
|
|
imageUrlParse(it)
|
|
}
|
|
}
|
|
|
|
private fun imageUrlRequest(page: Page): Request {
|
|
val jsonPayload = buildJsonObject {
|
|
put("urls", buildJsonArray { add(page.url) }.toString())
|
|
}
|
|
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
|
|
val newHeaders = headers.newBuilder()
|
|
.add("Content-Length", requestBody.contentLength().toString())
|
|
.add("Content-Type", requestBody.contentType().toString())
|
|
.build()
|
|
|
|
return POST(
|
|
"$baseUrl/$BASE_API_ENDPOINT/ImageToken?device=pc&platform=web",
|
|
headers = newHeaders,
|
|
body = requestBody,
|
|
)
|
|
}
|
|
|
|
private fun imageUrlParse(response: Response): String {
|
|
val result = with(json) {
|
|
response.parseAs<BilibiliResultDto<List<BilibiliPageDto>>>()
|
|
}
|
|
val page = result.data!![0]
|
|
|
|
return "${page.url}?token=${page.token}"
|
|
}
|
|
|
|
@Serializable
|
|
data class BilibiliPageDto(
|
|
val token: String,
|
|
val url: String,
|
|
)
|
|
|
|
@Serializable
|
|
data class BilibiliResultDto<T>(
|
|
val code: Int = 0,
|
|
val data: T? = null,
|
|
@SerialName("msg") val message: String = "",
|
|
)
|
|
|
|
@Serializable
|
|
data class BilibiliReader(
|
|
val images: List<BilibiliImageDto> = emptyList(),
|
|
)
|
|
|
|
@Serializable
|
|
data class BilibiliImageDto(
|
|
val path: String,
|
|
)
|
|
|
|
@Serializable
|
|
data class BilibiliComicDto(
|
|
@SerialName("author_name") val authorName: List<String> = emptyList(),
|
|
@SerialName("classic_lines") val classicLines: String = "",
|
|
@SerialName("comic_id") val comicId: Int = 0,
|
|
@SerialName("ep_list") val episodeList: List<BilibiliEpisodeDto> = emptyList(),
|
|
val id: Int = 0,
|
|
@SerialName("is_finish") val isFinish: Int = 0,
|
|
@SerialName("season_id") val seasonId: Int = 0,
|
|
val styles: List<String> = emptyList(),
|
|
val title: String,
|
|
@SerialName("vertical_cover") val verticalCover: String = "",
|
|
)
|
|
|
|
@Serializable
|
|
data class BilibiliEpisodeDto(
|
|
val id: Int,
|
|
@SerialName("is_locked") val isLocked: Boolean,
|
|
@SerialName("ord") val order: Float,
|
|
@SerialName("pub_time") val publicationTime: String,
|
|
val title: String,
|
|
)
|
|
|
|
companion object {
|
|
private const val BASE_API_ENDPOINT = "twirp/comic.v1.Comic"
|
|
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
|
private val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType()
|
|
}
|
|
}
|