MangaDex, add support for Bilibili chapters
This commit is contained in:
parent
7e162c99ce
commit
52cdb636c9
@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.lang.runAsObservable
|
|||||||
import exh.md.MangaDexFabHeaderAdapter
|
import exh.md.MangaDexFabHeaderAdapter
|
||||||
import exh.md.dto.MangaDto
|
import exh.md.dto.MangaDto
|
||||||
import exh.md.handlers.ApiMangaParser
|
import exh.md.handlers.ApiMangaParser
|
||||||
|
import exh.md.handlers.BilibiliHandler
|
||||||
import exh.md.handlers.ComikeyHandler
|
import exh.md.handlers.ComikeyHandler
|
||||||
import exh.md.handlers.FollowsHandler
|
import exh.md.handlers.FollowsHandler
|
||||||
import exh.md.handlers.MangaHandler
|
import exh.md.handlers.MangaHandler
|
||||||
@ -115,8 +116,19 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
private val comikeyHandler by lazy {
|
private val comikeyHandler by lazy {
|
||||||
ComikeyHandler(network.cloudflareClient)
|
ComikeyHandler(network.cloudflareClient)
|
||||||
}
|
}
|
||||||
|
private val bilibiliHandler by lazy {
|
||||||
|
BilibiliHandler(network.cloudflareClient)
|
||||||
|
}
|
||||||
private val pageHandler by lazy {
|
private val pageHandler by lazy {
|
||||||
PageHandler(headers, mangadexService, mangaPlusHandler, comikeyHandler, preferences, mdList)
|
PageHandler(
|
||||||
|
headers,
|
||||||
|
mangadexService,
|
||||||
|
mangaPlusHandler,
|
||||||
|
comikeyHandler,
|
||||||
|
bilibiliHandler,
|
||||||
|
preferences,
|
||||||
|
mdList
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlImportableSource methods
|
// UrlImportableSource methods
|
||||||
@ -168,6 +180,12 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||||
|
return pageHandler.fetchImageUrl(page) {
|
||||||
|
super.fetchImageUrl(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MetadataSource methods
|
// MetadataSource methods
|
||||||
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class
|
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class
|
||||||
|
|
||||||
|
238
app/src/main/java/exh/md/handlers/BilibiliHandler.kt
Normal file
238
app/src/main/java/exh/md/handlers/BilibiliHandler.kt
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
package exh.md.handlers
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptor
|
||||||
|
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.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 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()
|
||||||
|
.addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
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)).await()
|
||||||
|
return chapterListParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val result = 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.create().apply {
|
||||||
|
name = "Ep. " + episode.order.toString().removeSuffix(".0") +
|
||||||
|
" - " + episode.title
|
||||||
|
chapter_number = episode.order
|
||||||
|
url = "/mc$comicId/${episode.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchPageList(chapterUrl: String): List<Page> {
|
||||||
|
val response = client.newCall(pageListRequest(chapterUrl)).await()
|
||||||
|
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 = 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 = 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()
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ class PageHandler(
|
|||||||
private val service: MangaDexService,
|
private val service: MangaDexService,
|
||||||
private val mangaPlusHandler: MangaPlusHandler,
|
private val mangaPlusHandler: MangaPlusHandler,
|
||||||
private val comikeyHandler: ComikeyHandler,
|
private val comikeyHandler: ComikeyHandler,
|
||||||
|
private val bilibiliHandler: BilibiliHandler,
|
||||||
private val preferences: PreferencesHelper,
|
private val preferences: PreferencesHelper,
|
||||||
private val mdList: MdList,
|
private val mdList: MdList,
|
||||||
) {
|
) {
|
||||||
@ -42,7 +43,11 @@ class PageHandler(
|
|||||||
chapter.scanlator.equals("comikey", true) -> comikeyHandler.fetchPageList(
|
chapter.scanlator.equals("comikey", true) -> comikeyHandler.fetchPageList(
|
||||||
chapterResponse.data.attributes.externalUrl
|
chapterResponse.data.attributes.externalUrl
|
||||||
)
|
)
|
||||||
else -> throw Exception("Chapter not supported")
|
chapter.scanlator.equals("bilibili comics", true) -> bilibiliHandler.fetchPageList(
|
||||||
|
chapterResponse.data.attributes.externalUrl,
|
||||||
|
chapterResponse.data.attributes.chapter.toString()
|
||||||
|
)
|
||||||
|
else -> throw Exception("${chapter.scanlator} not supported")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val headers = if (isLogged) {
|
val headers = if (isLogged) {
|
||||||
@ -100,6 +105,7 @@ class PageHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun fetchImage(page: Page, superMethod: (Page) -> Observable<Response>): Observable<Response> {
|
fun fetchImage(page: Page, superMethod: (Page) -> Observable<Response>): Observable<Response> {
|
||||||
|
xLogD(page.imageUrl)
|
||||||
return when {
|
return when {
|
||||||
page.imageUrl?.contains("mangaplus", true) == true -> {
|
page.imageUrl?.contains("mangaplus", true) == true -> {
|
||||||
mangaPlusHandler.client.newCall(GET(page.imageUrl!!, headers))
|
mangaPlusHandler.client.newCall(GET(page.imageUrl!!, headers))
|
||||||
@ -109,6 +115,19 @@ class PageHandler(
|
|||||||
comikeyHandler.client.newCall(GET(page.imageUrl!!, comikeyHandler.headers))
|
comikeyHandler.client.newCall(GET(page.imageUrl!!, comikeyHandler.headers))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
}
|
}
|
||||||
|
page.imageUrl?.contains("/bfs/comic/", true) == true -> {
|
||||||
|
bilibiliHandler.client.newCall(GET(page.imageUrl!!, bilibiliHandler.headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
}
|
||||||
|
else -> superMethod(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchImageUrl(page: Page, superMethod: (Page) -> Observable<String>): Observable<String> {
|
||||||
|
return when {
|
||||||
|
page.url.contains("/bfs/comic/") -> {
|
||||||
|
bilibiliHandler.fetchImageUrl(page)
|
||||||
|
}
|
||||||
else -> superMethod(page)
|
else -> superMethod(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user