Remove Bilibili Manga and Kuaikanmanhua (#10255)
@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".zh.bilibilimanga.BilibiliUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
|
|
||||||
<data android:host="manga.bilibili.com" />
|
|
||||||
|
|
||||||
<data android:pathPattern="/detail/mc..*" />
|
|
||||||
<data android:pathPattern="/m/detail/mc..*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,46 +0,0 @@
|
|||||||
# Bilibili
|
|
||||||
|
|
||||||
Table of Content
|
|
||||||
- [FAQ](#FAQ)
|
|
||||||
- [Why are some chapters missing?](#why-are-some-chapters-missing)
|
|
||||||
- [Guides](#Guides)
|
|
||||||
- [Reading already paid chapters](#reading-already-paid-chapters)
|
|
||||||
|
|
||||||
Don't find the question you are looking for? Go check out our general FAQs and Guides
|
|
||||||
over at [Extension FAQ] or [Getting Started].
|
|
||||||
|
|
||||||
[Extension FAQ]: https://tachiyomi.org/help/faq/#extensions
|
|
||||||
[Getting Started]: https://tachiyomi.org/help/guides/getting-started/#installation
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
### Why are some chapters missing?
|
|
||||||
|
|
||||||
Bilibili now have series with paid chapters. These will be filtered out from
|
|
||||||
the chapter list by default if you didn't buy it before or if you're not signed in.
|
|
||||||
To sign in with your existing account, follow the guide available above.
|
|
||||||
|
|
||||||
## Guides
|
|
||||||
|
|
||||||
### Reading already paid chapters
|
|
||||||
|
|
||||||
The **Bilibili Comics** sources allows the reading of paid chapters in your account.
|
|
||||||
Follow the following steps to be able to sign in and get access to them:
|
|
||||||
|
|
||||||
1. Open the popular or latest section of the source.
|
|
||||||
2. Open the WebView by clicking the button with a globe icon.
|
|
||||||
3. Do the login with your existing account *(read the observations section)*.
|
|
||||||
4. Close the WebView and refresh the chapter list of the titles
|
|
||||||
you want to read the already paid chapters.
|
|
||||||
|
|
||||||
#### Observations
|
|
||||||
|
|
||||||
- Sign in with your Google account is not supported due to WebView restrictions
|
|
||||||
access that Google have. **You need to have a simple account in order to be able
|
|
||||||
to login via WebView**.
|
|
||||||
- You may sometime face the *"Failed to refresh the token"* error. To fix it,
|
|
||||||
you just need to open the WebView, await for the website to completely load.
|
|
||||||
After that, you can close the WebView and try again.
|
|
||||||
- The extension **will not** bypass any payment requirement. You still do need
|
|
||||||
to buy the chapters you want to read or wait until they become available and
|
|
||||||
added to your account.
|
|
@ -1,7 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'BILIBILI MANGA'
|
|
||||||
extClass = '.BilibiliManga'
|
|
||||||
extVersionCode = 13
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 34 KiB |
@ -1,520 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Base64
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
abstract class Bilibili(
|
|
||||||
override val name: String,
|
|
||||||
final override val baseUrl: String,
|
|
||||||
final override val lang: String,
|
|
||||||
) : HttpSource(), ConfigurableSource {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
||||||
.addInterceptor(::expiredImageTokenIntercept)
|
|
||||||
.addInterceptor(::decryptImageIntercept)
|
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 1)
|
|
||||||
.rateLimitHost(CDN_URL.toHttpUrl(), 2)
|
|
||||||
.rateLimitHost(MODIFIED_CDN_URL.toHttpUrl(), 2)
|
|
||||||
.rateLimitHost(COVER_CDN_URL.toHttpUrl(), 2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
|
||||||
.add("Accept", ACCEPT_JSON)
|
|
||||||
.add("Origin", baseUrl)
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
protected open val intl by lazy { BilibiliIntl(lang) }
|
|
||||||
|
|
||||||
private val apiLang: String = when (lang) {
|
|
||||||
BilibiliIntl.SIMPLIFIED_CHINESE -> "cn"
|
|
||||||
else -> lang
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open val defaultPopularSort: Int = 0
|
|
||||||
|
|
||||||
protected open val defaultLatestSort: Int = 1
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
|
||||||
|
|
||||||
protected val json: Json by injectLazy()
|
|
||||||
|
|
||||||
protected open val signedIn: Boolean = false
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(
|
|
||||||
page = page,
|
|
||||||
query = "",
|
|
||||||
filters = FilterList(
|
|
||||||
SortFilter("", getAllSortOptions(), defaultPopularSort),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(
|
|
||||||
page = page,
|
|
||||||
query = "",
|
|
||||||
filters = FilterList(
|
|
||||||
SortFilter("", getAllSortOptions(), defaultLatestSort),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
ID_SEARCH_PATTERN.matchEntire(query)?.let {
|
|
||||||
val (id) = it.destructured
|
|
||||||
val temporaryManga = SManga.create().apply { url = "/detail/mc$id" }
|
|
||||||
return mangaDetailsRequest(temporaryManga)
|
|
||||||
}
|
|
||||||
|
|
||||||
val price = filters.firstInstanceOrNull<PriceFilter>()?.state ?: 0
|
|
||||||
|
|
||||||
val jsonPayload = buildJsonObject {
|
|
||||||
put("area_id", filters.firstInstanceOrNull<AreaFilter>()?.selected?.id ?: -1)
|
|
||||||
put("is_finish", filters.firstInstanceOrNull<StatusFilter>()?.state?.minus(1) ?: -1)
|
|
||||||
put("is_free", if (price == 0) -1 else price)
|
|
||||||
put("order", filters.firstInstanceOrNull<SortFilter>()?.selected?.id ?: 0)
|
|
||||||
put("page_num", page)
|
|
||||||
put("page_size", if (query.isBlank()) POPULAR_PER_PAGE else SEARCH_PER_PAGE)
|
|
||||||
put("style_id", filters.firstInstanceOrNull<GenreFilter>()?.selected?.id ?: -1)
|
|
||||||
put("style_prefer", "[]")
|
|
||||||
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
put("need_shield_prefer", true)
|
|
||||||
put("key_word", query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val refererUrl = if (query.isBlank()) {
|
|
||||||
"$baseUrl/genre"
|
|
||||||
} else {
|
|
||||||
"$baseUrl/search".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("keyword", query)
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Referer", refererUrl)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment(if (query.isBlank()) "ClassPage" else "Search")
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, newHeaders, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val requestUrl = response.request.url.toString()
|
|
||||||
if (requestUrl.contains("ComicDetail")) {
|
|
||||||
val comic = mangaDetailsParse(response)
|
|
||||||
return MangasPage(listOf(comic), hasNextPage = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestUrl.contains("ClassPage")) {
|
|
||||||
val result = response.parseAs<List<BilibiliComicDto>>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return MangasPage(emptyList(), hasNextPage = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val comicList = result.data!!.map(::searchMangaFromObject)
|
|
||||||
val hasNextPage = comicList.size == POPULAR_PER_PAGE
|
|
||||||
|
|
||||||
return MangasPage(comicList, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = response.parseAs<BilibiliSearchDto>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return MangasPage(emptyList(), hasNextPage = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val comicList = result.data!!.list.map(::searchMangaFromObject)
|
|
||||||
val hasNextPage = comicList.size == SEARCH_PER_PAGE
|
|
||||||
|
|
||||||
return MangasPage(comicList, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply {
|
|
||||||
title = Jsoup.parse(comic.title).text()
|
|
||||||
thumbnail_url = comic.verticalCover + THUMBNAIL_RESOLUTION
|
|
||||||
|
|
||||||
val comicId = if (comic.id == 0) comic.seasonId else comic.id
|
|
||||||
url = "/detail/mc$comicId"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
val comicId = manga.url.substringAfterLast("/mc").toInt()
|
|
||||||
|
|
||||||
val jsonPayload = buildJsonObject { put("comic_id", comicId) }
|
|
||||||
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Referer", baseUrl + manga.url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/ComicDetail".toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, newHeaders, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
|
||||||
val comic = response.parseAs<BilibiliComicDto>().data!!
|
|
||||||
|
|
||||||
title = comic.title
|
|
||||||
author = comic.authorName.joinToString()
|
|
||||||
genre = comic.styles.joinToString()
|
|
||||||
status = when {
|
|
||||||
comic.isFinish == 1 -> SManga.COMPLETED
|
|
||||||
comic.isOnHiatus -> SManga.ON_HIATUS
|
|
||||||
else -> SManga.ONGOING
|
|
||||||
}
|
|
||||||
description = buildString {
|
|
||||||
if (comic.hasPaidChapters && !signedIn) {
|
|
||||||
append("${intl.hasPaidChaptersWarning(comic.paidChaptersCount)}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
append(comic.classicLines)
|
|
||||||
|
|
||||||
if (comic.updateWeekdays.isNotEmpty() && status == SManga.ONGOING) {
|
|
||||||
append("\n\n${intl.informationTitle}:")
|
|
||||||
append("\n• ${intl.getUpdateDays(comic.updateWeekdays)}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbnail_url = comic.verticalCover
|
|
||||||
url = "/detail/mc" + comic.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters are available in the same url of the manga details.
|
|
||||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val result = response.parseAs<BilibiliComicDto>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data!!.episodeList.map { ep -> chapterFromObject(ep, result.data.id) }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun chapterFromObject(
|
|
||||||
episode: BilibiliEpisodeDto,
|
|
||||||
comicId: Int,
|
|
||||||
isUnlocked: Boolean = false,
|
|
||||||
): SChapter = SChapter.create().apply {
|
|
||||||
name = buildString {
|
|
||||||
if (episode.isPaid && !isUnlocked) {
|
|
||||||
append("$EMOJI_LOCKED ")
|
|
||||||
}
|
|
||||||
|
|
||||||
append(episode.shortTitle)
|
|
||||||
|
|
||||||
if (episode.title.isNotBlank()) {
|
|
||||||
append(" - ${episode.title}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
date_upload = episode.publicationTime.toDate()
|
|
||||||
url = "/mc$comicId/${episode.id}"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request = imageIndexRequest(chapter.url, "")
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> = imageIndexParse(response)
|
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
|
||||||
protected open fun imageIndexRequest(chapterUrl: String, credential: String): Request {
|
|
||||||
val chapterId = chapterUrl.substringAfterLast("/").toInt()
|
|
||||||
|
|
||||||
val jsonPayload = buildJsonObject {
|
|
||||||
put("credential", credential)
|
|
||||||
put("ep_id", chapterId)
|
|
||||||
}
|
|
||||||
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Referer", baseUrl + chapterUrl)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/GetImageIndex".toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, newHeaders, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun imageIndexParse(response: Response): List<Page> {
|
|
||||||
val result = response.parseAs<BilibiliReader>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageQuality = preferences.chapterImageQuality
|
|
||||||
val imageFormat = preferences.chapterImageFormat
|
|
||||||
|
|
||||||
val imageUrls = result.data!!.images.map { it.url(imageQuality, imageFormat) }
|
|
||||||
val imageTokenRequest = imageTokenRequest(imageUrls)
|
|
||||||
val imageTokenResponse = client.newCall(imageTokenRequest).execute()
|
|
||||||
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
|
|
||||||
return imageTokenResult.data!!.zip(imageUrls).mapIndexed { i, pair ->
|
|
||||||
Page(i, pair.second, pair.first.imageUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun imageTokenRequest(urls: List<String>): Request {
|
|
||||||
val jsonPayload = buildJsonObject {
|
|
||||||
put("urls", json.encodeToString(urls))
|
|
||||||
}
|
|
||||||
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/ImageToken".toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, headers, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
val imageQualityPref = ListPreference(screen.context).apply {
|
|
||||||
key = "${IMAGE_QUALITY_PREF_KEY}_$lang"
|
|
||||||
title = intl.imageQualityPrefTitle
|
|
||||||
entries = intl.imageQualityPrefEntries
|
|
||||||
entryValues = IMAGE_QUALITY_PREF_ENTRY_VALUES
|
|
||||||
setDefaultValue(IMAGE_QUALITY_PREF_DEFAULT_VALUE)
|
|
||||||
summary = "%s"
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageFormatPref = ListPreference(screen.context).apply {
|
|
||||||
key = "${IMAGE_FORMAT_PREF_KEY}_$lang"
|
|
||||||
title = intl.imageFormatPrefTitle
|
|
||||||
entries = IMAGE_FORMAT_PREF_ENTRIES
|
|
||||||
entryValues = IMAGE_FORMAT_PREF_ENTRY_VALUES
|
|
||||||
setDefaultValue(IMAGE_FORMAT_PREF_DEFAULT_VALUE)
|
|
||||||
summary = "%s"
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.addPreference(imageQualityPref)
|
|
||||||
screen.addPreference(imageFormatPref)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun getAllGenres(): Array<BilibiliTag>
|
|
||||||
|
|
||||||
protected open fun getAllAreas(): Array<BilibiliTag> = emptyArray()
|
|
||||||
|
|
||||||
protected open fun getAllSortOptions(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag(intl.sortInterest, 0),
|
|
||||||
BilibiliTag(intl.sortUpdated, 4),
|
|
||||||
)
|
|
||||||
|
|
||||||
protected open fun getAllStatus(): Array<String> =
|
|
||||||
arrayOf(intl.statusAll, intl.statusOngoing, intl.statusComplete)
|
|
||||||
|
|
||||||
protected open fun getAllPrices(): Array<String> = emptyArray()
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
val allAreas = getAllAreas()
|
|
||||||
val allPrices = getAllPrices()
|
|
||||||
|
|
||||||
val filters = listOfNotNull(
|
|
||||||
StatusFilter(intl.statusLabel, getAllStatus()),
|
|
||||||
SortFilter(intl.sortLabel, getAllSortOptions(), defaultPopularSort),
|
|
||||||
PriceFilter(intl.priceLabel, getAllPrices()).takeIf { allPrices.isNotEmpty() },
|
|
||||||
GenreFilter(intl.genreLabel, getAllGenres()),
|
|
||||||
AreaFilter(intl.areaLabel, allAreas).takeIf { allAreas.isNotEmpty() },
|
|
||||||
)
|
|
||||||
|
|
||||||
return FilterList(filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
return super.imageRequest(page).newBuilder().tag(TAG_IMAGE_REQUEST)
|
|
||||||
.tag(TagImagePath::class.java, TagImagePath(page.url)).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decryptImageIntercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
if (response.isSuccessful && request.tag() == TAG_IMAGE_REQUEST) {
|
|
||||||
if (response.body.contentType()?.type == "image") {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
val cpx = request.url.queryParameter("cpx")
|
|
||||||
val iv = Base64.decode(cpx, Base64.DEFAULT).copyOfRange(60, 76)
|
|
||||||
val allBytes = response.body.bytes()
|
|
||||||
val size =
|
|
||||||
ByteBuffer.wrap(allBytes.copyOfRange(1, 5)).order(ByteOrder.BIG_ENDIAN).getInt()
|
|
||||||
val data = allBytes.copyOfRange(5, 5 + size)
|
|
||||||
val key = allBytes.copyOfRange(5 + size, allBytes.size)
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
val ivSpec = IvParameterSpec(iv)
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), ivSpec)
|
|
||||||
val encryptedSize = 20 * 1024 + 16
|
|
||||||
val decryptedSegment = cipher.doFinal(data, 0, encryptedSize.coerceAtMost(data.size))
|
|
||||||
val decryptedData = if (encryptedSize < data.size) {
|
|
||||||
// append remaining data
|
|
||||||
decryptedSegment + data.copyOfRange(encryptedSize, data.size)
|
|
||||||
} else {
|
|
||||||
decryptedSegment
|
|
||||||
}
|
|
||||||
val imageExtension = request.url.encodedPath.substringAfterLast(".", "jpg")
|
|
||||||
return response.newBuilder()
|
|
||||||
.body(decryptedData.toResponseBody("image/$imageExtension".toMediaType())).build()
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun expiredImageTokenIntercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
// Get a new image token if the current one expired.
|
|
||||||
if (response.code == 400 && request.tag() == TAG_IMAGE_REQUEST) {
|
|
||||||
val imagePath = request.tag(TagImagePath::class)
|
|
||||||
if (imagePath?.path.isNullOrEmpty()) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
response.close()
|
|
||||||
val imageTokenRequest = imageTokenRequest(listOf(imagePath!!.path))
|
|
||||||
val imageTokenResponse = chain.proceed(imageTokenRequest)
|
|
||||||
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
|
|
||||||
imageTokenResponse.close()
|
|
||||||
|
|
||||||
val newPage = imageTokenResult.data!!.first()
|
|
||||||
val newPageUrl = newPage.imageUrl
|
|
||||||
|
|
||||||
val newRequest = imageRequest(Page(0, imagePath.path, newPageUrl))
|
|
||||||
|
|
||||||
return chain.proceed(newRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SharedPreferences.chapterImageQuality
|
|
||||||
get() = when (
|
|
||||||
getString(
|
|
||||||
"${IMAGE_QUALITY_PREF_KEY}_$lang",
|
|
||||||
IMAGE_QUALITY_PREF_DEFAULT_VALUE,
|
|
||||||
)!!
|
|
||||||
) {
|
|
||||||
"hd" -> "1600w"
|
|
||||||
"sd" -> "1000w"
|
|
||||||
"low" -> "800w_50q"
|
|
||||||
else -> "raw"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SharedPreferences.chapterImageFormat
|
|
||||||
get() = getString("${IMAGE_FORMAT_PREF_KEY}_$lang", IMAGE_FORMAT_PREF_DEFAULT_VALUE)!!
|
|
||||||
|
|
||||||
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? = firstOrNull { it is R } as? R
|
|
||||||
|
|
||||||
protected open fun HttpUrl.Builder.addCommonParameters(): HttpUrl.Builder = apply {
|
|
||||||
if (name == "BILIBILI COMICS") {
|
|
||||||
addQueryParameter("lang", apiLang)
|
|
||||||
addQueryParameter("sys_lang", apiLang)
|
|
||||||
}
|
|
||||||
|
|
||||||
addQueryParameter("device", "pc")
|
|
||||||
addQueryParameter("platform", "web")
|
|
||||||
}
|
|
||||||
|
|
||||||
protected inline fun <reified T> Response.parseAs(): BilibiliResultDto<T> = use {
|
|
||||||
json.decodeFromString(it.body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.toDate(): Long {
|
|
||||||
return runCatching { DATE_FORMATTER.parse(this)?.time }
|
|
||||||
.getOrNull() ?: 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TagImagePath(val path: String)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val CDN_URL = "https://manga.hdslb.com"
|
|
||||||
const val MODIFIED_CDN_URL = "https://mangaup.hdslb.com"
|
|
||||||
const val COVER_CDN_URL = "https://i0.hdslb.com"
|
|
||||||
|
|
||||||
const val API_COMIC_V1_COMIC_ENDPOINT = "twirp/comic.v1.Comic"
|
|
||||||
|
|
||||||
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
|
||||||
private const val TAG_IMAGE_REQUEST = "tag_image_request"
|
|
||||||
|
|
||||||
val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType()
|
|
||||||
|
|
||||||
private const val POPULAR_PER_PAGE = 18
|
|
||||||
private const val SEARCH_PER_PAGE = 9
|
|
||||||
|
|
||||||
const val PREFIX_ID_SEARCH = "id:"
|
|
||||||
private val ID_SEARCH_PATTERN = "^${PREFIX_ID_SEARCH}mc(\\d+)$".toRegex()
|
|
||||||
|
|
||||||
private const val IMAGE_QUALITY_PREF_KEY = "chapterImageQuality"
|
|
||||||
private val IMAGE_QUALITY_PREF_ENTRY_VALUES = arrayOf("raw", "hd", "sd", "low")
|
|
||||||
private val IMAGE_QUALITY_PREF_DEFAULT_VALUE = IMAGE_QUALITY_PREF_ENTRY_VALUES[1]
|
|
||||||
|
|
||||||
private const val IMAGE_FORMAT_PREF_KEY = "chapterImageFormat"
|
|
||||||
private val IMAGE_FORMAT_PREF_ENTRIES = arrayOf("JPG", "WEBP", "PNG")
|
|
||||||
private val IMAGE_FORMAT_PREF_ENTRY_VALUES = arrayOf("jpg", "webp", "png")
|
|
||||||
private val IMAGE_FORMAT_PREF_DEFAULT_VALUE = IMAGE_FORMAT_PREF_ENTRY_VALUES[0]
|
|
||||||
|
|
||||||
const val THUMBNAIL_RESOLUTION = "@512w.jpg"
|
|
||||||
|
|
||||||
private val DATE_FORMATTER by lazy {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val EMOJI_LOCKED = "\uD83D\uDD12"
|
|
||||||
const val EMOJI_WARNING = "\u26A0\uFE0F"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliResultDto<T>(
|
|
||||||
val code: Int = 0,
|
|
||||||
val data: T? = null,
|
|
||||||
@SerialName("msg") val message: String = "",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliSearchDto(
|
|
||||||
val list: List<BilibiliComicDto> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@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("temp_stop_update") val isOnHiatus: Boolean = false,
|
|
||||||
@SerialName("season_id") val seasonId: Int = 0,
|
|
||||||
val styles: List<String> = emptyList(),
|
|
||||||
val title: String,
|
|
||||||
@SerialName("update_weekday") val updateWeekdays: List<Int> = emptyList(),
|
|
||||||
@SerialName("vertical_cover") val verticalCover: String = "",
|
|
||||||
) {
|
|
||||||
val hasPaidChapters: Boolean
|
|
||||||
get() = paidChaptersCount > 0
|
|
||||||
|
|
||||||
val paidChaptersCount: Int
|
|
||||||
get() = episodeList.filter { it.isPaid }.size
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliEpisodeDto(
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("is_in_free") val isInFree: Boolean,
|
|
||||||
@SerialName("is_locked") val isLocked: Boolean,
|
|
||||||
@SerialName("pay_gold") val payGold: Int,
|
|
||||||
@SerialName("pay_mode") val payMode: Int,
|
|
||||||
@SerialName("pub_time") val publicationTime: String,
|
|
||||||
@SerialName("short_title") val shortTitle: String,
|
|
||||||
val title: String,
|
|
||||||
) {
|
|
||||||
val isPaid = payMode == 1 && payGold > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliReader(
|
|
||||||
val images: List<BilibiliImageDto> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliImageDto(
|
|
||||||
val path: String,
|
|
||||||
@SerialName("x") val width: Int,
|
|
||||||
@SerialName("y") val height: Int,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun url(quality: String, format: String): String {
|
|
||||||
val imageWidth = if (quality == "raw") "${width}w" else quality
|
|
||||||
|
|
||||||
return "$path@$imageWidth.$format"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliPageDto(
|
|
||||||
val token: String,
|
|
||||||
val url: String,
|
|
||||||
@SerialName("complete_url")
|
|
||||||
val completeUrl: String,
|
|
||||||
) {
|
|
||||||
val imageUrl: String
|
|
||||||
get() = completeUrl.ifEmpty { "$url?token=$token" }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliAccessTokenCookie(
|
|
||||||
val accessToken: String,
|
|
||||||
val refreshToken: String,
|
|
||||||
val area: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliAccessToken(
|
|
||||||
@SerialName("access_token") val accessToken: String,
|
|
||||||
@SerialName("refresh_token") val refreshToken: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliUserEpisodes(
|
|
||||||
@SerialName("unlocked_eps") val unlockedEpisodes: List<BilibiliUnlockedEpisode>? = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliUnlockedEpisode(
|
|
||||||
@SerialName("ep_id") val id: Int = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliGetCredential(
|
|
||||||
@SerialName("comic_id") val comicId: Int,
|
|
||||||
@SerialName("ep_id") val episodeId: Int,
|
|
||||||
val type: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BilibiliCredential(
|
|
||||||
val credential: String,
|
|
||||||
)
|
|
@ -1,29 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
|
|
||||||
data class BilibiliTag(val name: String, val id: Int) {
|
|
||||||
override fun toString(): String = name
|
|
||||||
}
|
|
||||||
|
|
||||||
open class EnhancedSelect<T>(name: String, values: Array<T>, state: Int = 0) :
|
|
||||||
Filter.Select<T>(name, values, state) {
|
|
||||||
|
|
||||||
val selected: T?
|
|
||||||
get() = values.getOrNull(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenreFilter(label: String, genres: Array<BilibiliTag>) :
|
|
||||||
EnhancedSelect<BilibiliTag>(label, genres)
|
|
||||||
|
|
||||||
class AreaFilter(label: String, genres: Array<BilibiliTag>) :
|
|
||||||
EnhancedSelect<BilibiliTag>(label, genres)
|
|
||||||
|
|
||||||
class SortFilter(label: String, options: Array<BilibiliTag>, state: Int = 0) :
|
|
||||||
EnhancedSelect<BilibiliTag>(label, options, state)
|
|
||||||
|
|
||||||
class StatusFilter(label: String, statuses: Array<String>) :
|
|
||||||
Filter.Select<String>(label, statuses)
|
|
||||||
|
|
||||||
class PriceFilter(label: String, prices: Array<String>) :
|
|
||||||
Filter.Select<String>(label, prices)
|
|
@ -1,226 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
|
||||||
|
|
||||||
import java.text.DateFormatSymbols
|
|
||||||
import java.text.NumberFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class BilibiliIntl(private val lang: String) {
|
|
||||||
|
|
||||||
private val locale by lazy { Locale.forLanguageTag(lang) }
|
|
||||||
|
|
||||||
private val dateFormatSymbols by lazy { DateFormatSymbols(locale) }
|
|
||||||
|
|
||||||
private val numberFormat by lazy { NumberFormat.getInstance(locale) }
|
|
||||||
|
|
||||||
val statusLabel: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "进度"
|
|
||||||
SPANISH -> "Estado"
|
|
||||||
else -> "Status"
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortLabel: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "排序"
|
|
||||||
INDONESIAN -> "Urutkan dengan"
|
|
||||||
SPANISH -> "Ordenar por"
|
|
||||||
else -> "Sort by"
|
|
||||||
}
|
|
||||||
|
|
||||||
val genreLabel: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "题材"
|
|
||||||
SPANISH -> "Género"
|
|
||||||
else -> "Genre"
|
|
||||||
}
|
|
||||||
|
|
||||||
val areaLabel: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "地区"
|
|
||||||
else -> "Area"
|
|
||||||
}
|
|
||||||
|
|
||||||
val priceLabel: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "收费"
|
|
||||||
INDONESIAN -> "Harga"
|
|
||||||
SPANISH -> "Precio"
|
|
||||||
else -> "Price"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasPaidChaptersWarning(chapterCount: Int): String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE ->
|
|
||||||
"${Bilibili.EMOJI_WARNING} 此漫画有 ${chapterCount.localized} 个付费章节,已在目录中隐藏。" +
|
|
||||||
"如果你已购买,请在 WebView 登录并刷新目录,即可阅读已购章节。"
|
|
||||||
SPANISH ->
|
|
||||||
"${Bilibili.EMOJI_WARNING} ADVERTENCIA: Esta serie tiene ${chapterCount.localized} " +
|
|
||||||
"capítulos pagos que fueron filtrados de la lista de capítulos. Si ya has " +
|
|
||||||
"desbloqueado y tiene alguno en su cuenta, inicie sesión en WebView y " +
|
|
||||||
"actualice la lista de capítulos para leerlos."
|
|
||||||
else ->
|
|
||||||
"${Bilibili.EMOJI_WARNING} WARNING: This series has ${chapterCount.localized} paid " +
|
|
||||||
"chapters. If you have any unlocked in your account then sign in through WebView " +
|
|
||||||
"to be able to read them."
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageQualityPrefTitle: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "章节图片质量"
|
|
||||||
INDONESIAN -> "Kualitas gambar"
|
|
||||||
SPANISH -> "Calidad de imagen del capítulo"
|
|
||||||
else -> "Chapter image quality"
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageQualityPrefEntries: Array<String> = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> arrayOf("原图", "高清 (1600w)", "标清 (1000w)", "低清 (800w)")
|
|
||||||
else -> arrayOf("Raw", "HD (1600w)", "SD (1000w)", "Low (800w)")
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageFormatPrefTitle: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "章节图片格式"
|
|
||||||
INDONESIAN -> "Format gambar"
|
|
||||||
SPANISH -> "Formato de la imagen del capítulo"
|
|
||||||
else -> "Chapter image format"
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortInterest: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "为你推荐"
|
|
||||||
INDONESIAN -> "Kamu Mungkin Suka"
|
|
||||||
SPANISH -> "Sugerencia"
|
|
||||||
else -> "Interest"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliManga
|
|
||||||
val sortPopular: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "人气推荐"
|
|
||||||
INDONESIAN -> "Populer"
|
|
||||||
SPANISH -> "Popularidad"
|
|
||||||
FRENCH -> "Préférences"
|
|
||||||
else -> "Popular"
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortUpdated: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "更新时间"
|
|
||||||
INDONESIAN -> "Terbaru"
|
|
||||||
SPANISH -> "Actualización"
|
|
||||||
FRENCH -> "Récent"
|
|
||||||
else -> "Updated"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliManga
|
|
||||||
val sortAdded: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "上架时间"
|
|
||||||
else -> "Added"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliManga
|
|
||||||
val sortFollowers: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "追漫人数"
|
|
||||||
else -> "Followers count"
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusAll: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "全部"
|
|
||||||
INDONESIAN -> "Semua"
|
|
||||||
SPANISH -> "Todos"
|
|
||||||
FRENCH -> "Tout"
|
|
||||||
else -> "All"
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusOngoing: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "连载中"
|
|
||||||
INDONESIAN -> "Berlangsung"
|
|
||||||
SPANISH -> "En curso"
|
|
||||||
FRENCH -> "En cours"
|
|
||||||
else -> "Ongoing"
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusComplete: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "已完结"
|
|
||||||
INDONESIAN -> "Tamat"
|
|
||||||
SPANISH -> "Finalizado"
|
|
||||||
FRENCH -> "Complet"
|
|
||||||
else -> "Completed"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliManga
|
|
||||||
val priceAll: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "全部"
|
|
||||||
INDONESIAN -> "Semua"
|
|
||||||
SPANISH -> "Todos"
|
|
||||||
else -> "All"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliManga
|
|
||||||
val priceFree: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "免费"
|
|
||||||
INDONESIAN -> "Bebas"
|
|
||||||
SPANISH -> "Gratis"
|
|
||||||
else -> "Free"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliManga
|
|
||||||
val pricePaid: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "付费"
|
|
||||||
INDONESIAN -> "Dibayar"
|
|
||||||
SPANISH -> "Pago"
|
|
||||||
else -> "Paid"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliManga
|
|
||||||
val priceWaitForFree: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "等就免费"
|
|
||||||
else -> "Wait for free"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliComics
|
|
||||||
val failedToRefreshToken: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "无法刷新令牌。请打开 WebView 修正错误。"
|
|
||||||
SPANISH -> "Error al actualizar el token. Abra el WebView para solucionar este error."
|
|
||||||
else -> "Failed to refresh the token. Open the WebView to fix this error."
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliComics
|
|
||||||
val failedToGetCredential: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "无法获取阅读章节所需的凭证。"
|
|
||||||
SPANISH -> "Erro al obtener la credencial para leer el capítulo."
|
|
||||||
else -> "Failed to get the credential to read the chapter."
|
|
||||||
}
|
|
||||||
|
|
||||||
val informationTitle: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "信息"
|
|
||||||
SPANISH -> "Información"
|
|
||||||
else -> "Information"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val updatesDaily: String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "每日更新"
|
|
||||||
SPANISH -> "Actualizaciones diarias"
|
|
||||||
else -> "Updates daily"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatesEvery(days: String): String = when (lang) {
|
|
||||||
CHINESE, SIMPLIFIED_CHINESE -> "${days}更新"
|
|
||||||
SPANISH -> "Actualizaciones todos los $days"
|
|
||||||
else -> "Updates every $days"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUpdateDays(dayIndexes: List<Int>): String {
|
|
||||||
val shortWeekDays = dateFormatSymbols.shortWeekdays.filterNot(String::isBlank)
|
|
||||||
if (dayIndexes.size == shortWeekDays.size) return updatesDaily
|
|
||||||
val shortWeekDaysUpperCased = shortWeekDays.map {
|
|
||||||
it.replaceFirstChar { char -> char.uppercase(locale) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val days = dayIndexes.joinToString { shortWeekDaysUpperCased[it] }
|
|
||||||
return updatesEvery(days)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Int.localized: String
|
|
||||||
get() = numberFormat.format(this)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val CHINESE = "zh"
|
|
||||||
const val INDONESIAN = "id"
|
|
||||||
const val SIMPLIFIED_CHINESE = "zh-Hans"
|
|
||||||
const val SPANISH = "es"
|
|
||||||
const val FRENCH = "fr"
|
|
||||||
|
|
||||||
@Suppress("UNUSED") // In BilibiliComics
|
|
||||||
const val ENGLISH = "en"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class BilibiliManga : Bilibili(
|
|
||||||
"哔哩哔哩漫画",
|
|
||||||
"https://manga.bilibili.com",
|
|
||||||
BilibiliIntl.SIMPLIFIED_CHINESE,
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val id: Long = 3561131545129718586
|
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
|
||||||
add("User-Agent", DEFAULT_USER_AGENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val result = response.parseAs<BilibiliComicDto>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val data = result.data!!
|
|
||||||
val id = data.id
|
|
||||||
return data.episodeList.mapNotNull { episode ->
|
|
||||||
if (episode.isInFree || !episode.isLocked) {
|
|
||||||
chapterFromObject(episode, id)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultPopularSort: Int = 0
|
|
||||||
|
|
||||||
override val defaultLatestSort: Int = 1
|
|
||||||
|
|
||||||
override fun getAllSortOptions(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag(intl.sortPopular, 0),
|
|
||||||
BilibiliTag(intl.sortUpdated, 1),
|
|
||||||
BilibiliTag(intl.sortFollowers, 2),
|
|
||||||
BilibiliTag(intl.sortAdded, 3),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAllPrices(): Array<String> =
|
|
||||||
arrayOf(intl.priceAll, intl.priceFree, intl.pricePaid, intl.priceWaitForFree)
|
|
||||||
|
|
||||||
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("全部", -1),
|
|
||||||
BilibiliTag("竞技", 1034),
|
|
||||||
BilibiliTag("冒险", 1013),
|
|
||||||
BilibiliTag("热血", 999),
|
|
||||||
BilibiliTag("搞笑", 994),
|
|
||||||
BilibiliTag("恋爱", 995),
|
|
||||||
BilibiliTag("少女", 1026),
|
|
||||||
BilibiliTag("日常", 1020),
|
|
||||||
BilibiliTag("校园", 1001),
|
|
||||||
BilibiliTag("治愈", 1007),
|
|
||||||
BilibiliTag("古风", 997),
|
|
||||||
BilibiliTag("玄幻", 1016),
|
|
||||||
BilibiliTag("奇幻", 998),
|
|
||||||
BilibiliTag("惊奇", 996),
|
|
||||||
BilibiliTag("悬疑", 1023),
|
|
||||||
BilibiliTag("都市", 1002),
|
|
||||||
BilibiliTag("剧情", 1030),
|
|
||||||
BilibiliTag("总裁", 1004),
|
|
||||||
BilibiliTag("科幻", 1015),
|
|
||||||
BilibiliTag("正能量", 1028),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAllAreas(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("全部", -1),
|
|
||||||
BilibiliTag("大陆", 1),
|
|
||||||
BilibiliTag("日本", 2),
|
|
||||||
BilibiliTag("韩国", 6),
|
|
||||||
BilibiliTag("其他", 5),
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Springboard that accepts https://www.bilibilicomics.com/detail/xxx intents and redirects them to
|
|
||||||
* the main tachiyomi process. The idea is to not install the intent filter unless
|
|
||||||
* you have this extension installed, but still let the main tachiyomi app control
|
|
||||||
* things.
|
|
||||||
*
|
|
||||||
* Main goal was to make it easier to open manga in Tachiyomi in spite of the DDoS blocking
|
|
||||||
* the usual search screen from working.
|
|
||||||
*/
|
|
||||||
class BilibiliUrlActivity : Activity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
|
||||||
// Mobile site of https://manga.bilibili.com starts with path "m"
|
|
||||||
val titleId = if (pathSegments[0] == "m") pathSegments[2] else pathSegments[1]
|
|
||||||
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
|
||||||
putExtra("query", Bilibili.PREFIX_ID_SEARCH + titleId)
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(mainIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e("BilibiliUrlActivity", e.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("BilibiliUrlActivity", "Could not parse URI from intent $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".zh.kuaikanmanhua.KuaikanmanhuaUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:host="*.kuaikanmanhua.com" />
|
|
||||||
<data android:host="kuaikanmanhua.com" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="m.kuaikanmanhua.com"
|
|
||||||
android:pathPattern="/mobile/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:pathPattern="/web/topic/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,7 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Kuaikanmanhua'
|
|
||||||
extClass = '.Kuaikanmanhua'
|
|
||||||
extVersionCode = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 22 KiB |
@ -1,285 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.kuaikanmanhua
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.boolean
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class Kuaikanmanhua : HttpSource() {
|
|
||||||
|
|
||||||
override val name = "快看漫画"
|
|
||||||
|
|
||||||
override val id: Long = 8099870292642776005
|
|
||||||
|
|
||||||
override val baseUrl = "https://www.kuaikanmanhua.com"
|
|
||||||
|
|
||||||
override val lang = "zh"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
|
||||||
|
|
||||||
private val apiUrl = "https://api.kkmh.com"
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return GET("$apiUrl/v1/topic_new/lists/get_by_tag?tag=0&since=${(page - 1) * 10}", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val body = response.body.string()
|
|
||||||
val jsonList = json.parseToJsonElement(body).jsonObject["data"]!!
|
|
||||||
.jsonObject["topics"]!!
|
|
||||||
.jsonArray
|
|
||||||
return parseMangaJsonArray(jsonList)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMangaJsonArray(jsonList: JsonArray, isSearch: Boolean = false): MangasPage {
|
|
||||||
val mangaList = jsonList.map {
|
|
||||||
val mangaObj = it.jsonObject
|
|
||||||
|
|
||||||
SManga.create().apply {
|
|
||||||
title = mangaObj["title"]!!.jsonPrimitive.content
|
|
||||||
thumbnail_url = mangaObj["vertical_image_url"]!!.jsonPrimitive.content
|
|
||||||
url = "/web/topic/" + mangaObj["id"]!!.jsonPrimitive.int
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KKMH does not have pages when you search
|
|
||||||
return MangasPage(mangaList, hasNextPage = mangaList.size > 9 && !isSearch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return GET("$apiUrl/v1/topic_new/lists/get_by_tag?tag=19&since=${(page - 1) * 10}", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith(TOPIC_ID_SEARCH_PREFIX)) {
|
|
||||||
val newQuery = query.removePrefix(TOPIC_ID_SEARCH_PREFIX)
|
|
||||||
return client.newCall(GET("$apiUrl/v1/topics/$newQuery"))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
val details = mangaDetailsParse(response)
|
|
||||||
details.url = "/web/topic/$newQuery"
|
|
||||||
MangasPage(listOf(details), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
return if (query.isNotEmpty()) {
|
|
||||||
GET("$apiUrl/v1/search/topic?q=$query&size=18", headers)
|
|
||||||
} else {
|
|
||||||
lateinit var genre: String
|
|
||||||
lateinit var status: String
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is GenreFilter -> {
|
|
||||||
genre = filter.toUriPart()
|
|
||||||
}
|
|
||||||
is StatusFilter -> {
|
|
||||||
status = filter.toUriPart()
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GET("$apiUrl/v1/search/by_tag?since=${(page - 1) * 10}&tag=$genre&sort=1&query_category=%7B%22update_status%22:$status%7D")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val body = response.body.string()
|
|
||||||
val jsonObj = json.parseToJsonElement(body).jsonObject["data"]!!.jsonObject
|
|
||||||
if (jsonObj["hit"] != null) {
|
|
||||||
return parseMangaJsonArray(jsonObj["hit"]!!.jsonArray, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseMangaJsonArray(jsonObj["topics"]!!.jsonArray, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
// Convert the stored url to one that works with the api
|
|
||||||
val newUrl = apiUrl + "/v1/topics/" + manga.url.trimEnd('/').substringAfterLast("/")
|
|
||||||
val response = client.newCall(GET(newUrl)).execute()
|
|
||||||
val sManga = mangaDetailsParse(response).apply { initialized = true }
|
|
||||||
return Observable.just(sManga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
|
||||||
val data = json.parseToJsonElement(response.body.string())
|
|
||||||
.jsonObject["data"]!!
|
|
||||||
.jsonObject
|
|
||||||
|
|
||||||
title = data["title"]!!.jsonPrimitive.content
|
|
||||||
thumbnail_url = data["vertical_image_url"]!!.jsonPrimitive.content
|
|
||||||
author = data["user"]!!.jsonObject["nickname"]!!.jsonPrimitive.content
|
|
||||||
description = data["description"]!!.jsonPrimitive.content
|
|
||||||
status = data["update_status_code"]!!.jsonPrimitive.int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters & Pages
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
val newUrl = apiUrl + "/v1/topics/" + manga.url.trimEnd('/').substringAfterLast("/")
|
|
||||||
val response = client.newCall(GET(newUrl)).execute()
|
|
||||||
val chapters = chapterListParse(response)
|
|
||||||
return Observable.just(chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val data = json.parseToJsonElement(response.body.string())
|
|
||||||
.jsonObject["data"]!!
|
|
||||||
.jsonObject
|
|
||||||
val chaptersJson = data["comics"]!!.jsonArray
|
|
||||||
val chapters = mutableListOf<SChapter>()
|
|
||||||
|
|
||||||
for (i in 0 until chaptersJson.size) {
|
|
||||||
val obj = chaptersJson[i].jsonObject
|
|
||||||
chapters.add(
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = "/web/comic/" + obj["id"]!!.jsonPrimitive.content
|
|
||||||
name = obj["title"]!!.jsonPrimitive.content +
|
|
||||||
if (!obj["can_view"]!!.jsonPrimitive.boolean) {
|
|
||||||
" \uD83D\uDD12"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
date_upload = obj["created_at"]!!.jsonPrimitive.long * 1000
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
val request = client.newCall(pageListRequest(chapter)).execute()
|
|
||||||
return Observable.just(pageListParse(request))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
// if (chapter.name.endsWith("🔒")) {
|
|
||||||
// throw Exception("[此章节为付费内容]")
|
|
||||||
// }
|
|
||||||
return GET(baseUrl + chapter.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val fixJson: (MatchResult) -> CharSequence = {
|
|
||||||
match: MatchResult ->
|
|
||||||
val str = match.value
|
|
||||||
val out = str[0] + "\"" + str.subSequence(1, str.length - 1) + "\"" + str[str.length - 1]
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
val script = document.selectFirst("script:containsData(comicImages)")!!.data()
|
|
||||||
val images = script.substringAfter("comicImages:")
|
|
||||||
.substringBefore(",is_vip_exclusive")
|
|
||||||
.replace("""(:([^\[\{\"]+?)[\},])""".toRegex(), fixJson)
|
|
||||||
.replace("""([,{]([^\[\{\"]+?)[\}:])""".toRegex(), fixJson)
|
|
||||||
.let { json.parseToJsonElement(it).jsonArray }
|
|
||||||
val variable = script.substringAfter("(function(")
|
|
||||||
.substringBefore("){")
|
|
||||||
.split(",")
|
|
||||||
val value = script.substringAfterLast("}}(")
|
|
||||||
.substringBefore("));")
|
|
||||||
.split(",")
|
|
||||||
|
|
||||||
return images.mapIndexed { index, jsonEl ->
|
|
||||||
val urlVar = jsonEl.jsonObject["url"]!!.jsonPrimitive.content
|
|
||||||
val imageUrl = value[variable.indexOf(urlVar)]
|
|
||||||
.replace("\\u002F", "/")
|
|
||||||
.replace("\"", "")
|
|
||||||
|
|
||||||
Page(index, "", imageUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
Filter.Header("注意:不影響按標題搜索"),
|
|
||||||
StatusFilter(),
|
|
||||||
GenreFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GenreFilter : UriPartFilter(
|
|
||||||
"题材",
|
|
||||||
arrayOf(
|
|
||||||
Pair("全部", "0"),
|
|
||||||
Pair("恋爱", "20"),
|
|
||||||
Pair("古风", "46"),
|
|
||||||
Pair("校园", "47"),
|
|
||||||
Pair("奇幻", "22"),
|
|
||||||
Pair("大女主", "77"),
|
|
||||||
Pair("治愈", "27"),
|
|
||||||
Pair("总裁", "52"),
|
|
||||||
Pair("完结", "40"),
|
|
||||||
Pair("唯美", "58"),
|
|
||||||
Pair("日漫", "57"),
|
|
||||||
Pair("韩漫", "60"),
|
|
||||||
Pair("穿越", "80"),
|
|
||||||
Pair("正能量", "54"),
|
|
||||||
Pair("灵异", "32"),
|
|
||||||
Pair("爆笑", "24"),
|
|
||||||
Pair("都市", "48"),
|
|
||||||
Pair("萌系", "62"),
|
|
||||||
Pair("玄幻", "63"),
|
|
||||||
Pair("日常", "19"),
|
|
||||||
Pair("投稿", "76"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class StatusFilter : UriPartFilter(
|
|
||||||
"类别",
|
|
||||||
arrayOf(
|
|
||||||
Pair("全部", "1"),
|
|
||||||
Pair("连载中", "2"),
|
|
||||||
Pair("已完结", "3"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
|
||||||
fun toUriPart() = vals[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TOPIC_ID_SEARCH_PREFIX = "topic:"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.kuaikanmanhua
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class KuaikanmanhuaUrlActivity : Activity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val host = intent?.data?.host
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
|
||||||
val id = when (host) {
|
|
||||||
"m.kuaikanmanhua.com" -> pathSegments[1]
|
|
||||||
else -> pathSegments[2]
|
|
||||||
}
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
|
||||||
putExtra("query", "${Kuaikanmanhua.TOPIC_ID_SEARCH_PREFIX}$id")
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(mainIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e("KkmhUrlActivity", e.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("KkmhUrlActivity", "could not parse uri from intent $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|