diff --git a/src/zh/bilibilimanga/AndroidManifest.xml b/src/zh/bilibilimanga/AndroidManifest.xml
deleted file mode 100644
index 37cf7b4d8..000000000
--- a/src/zh/bilibilimanga/AndroidManifest.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/zh/bilibilimanga/README.md b/src/zh/bilibilimanga/README.md
deleted file mode 100644
index 003a4b3bf..000000000
--- a/src/zh/bilibilimanga/README.md
+++ /dev/null
@@ -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.
diff --git a/src/zh/bilibilimanga/build.gradle b/src/zh/bilibilimanga/build.gradle
deleted file mode 100644
index 96fe55c39..000000000
--- a/src/zh/bilibilimanga/build.gradle
+++ /dev/null
@@ -1,7 +0,0 @@
-ext {
- extName = 'BILIBILI MANGA'
- extClass = '.BilibiliManga'
- extVersionCode = 13
-}
-
-apply from: "$rootDir/common.gradle"
diff --git a/src/zh/bilibilimanga/res/mipmap-hdpi/ic_launcher.png b/src/zh/bilibilimanga/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index db7eed80c..000000000
Binary files a/src/zh/bilibilimanga/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/bilibilimanga/res/mipmap-mdpi/ic_launcher.png b/src/zh/bilibilimanga/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index f14014676..000000000
Binary files a/src/zh/bilibilimanga/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/bilibilimanga/res/mipmap-xhdpi/ic_launcher.png b/src/zh/bilibilimanga/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 426304e5c..000000000
Binary files a/src/zh/bilibilimanga/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/bilibilimanga/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/bilibilimanga/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index ed35c4c57..000000000
Binary files a/src/zh/bilibilimanga/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/bilibilimanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/bilibilimanga/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 600a188b8..000000000
Binary files a/src/zh/bilibilimanga/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/Bilibili.kt b/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/Bilibili.kt
deleted file mode 100644
index 7a6e4b6f2..000000000
--- a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/Bilibili.kt
+++ /dev/null
@@ -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()?.state ?: 0
-
- val jsonPayload = buildJsonObject {
- put("area_id", filters.firstInstanceOrNull()?.selected?.id ?: -1)
- put("is_finish", filters.firstInstanceOrNull()?.state?.minus(1) ?: -1)
- put("is_free", if (price == 0) -1 else price)
- put("order", filters.firstInstanceOrNull()?.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()?.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>()
-
- 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()
-
- 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().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 {
- val result = response.parseAs()
-
- 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 = 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 {
- val result = response.parseAs()
-
- 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>()
- return imageTokenResult.data!!.zip(imageUrls).mapIndexed { i, pair ->
- Page(i, pair.second, pair.first.imageUrl)
- }
- }
-
- protected open fun imageTokenRequest(urls: List): 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
-
- protected open fun getAllAreas(): Array = emptyArray()
-
- protected open fun getAllSortOptions(): Array = arrayOf(
- BilibiliTag(intl.sortInterest, 0),
- BilibiliTag(intl.sortUpdated, 4),
- )
-
- protected open fun getAllStatus(): Array =
- arrayOf(intl.statusAll, intl.statusOngoing, intl.statusComplete)
-
- protected open fun getAllPrices(): Array = 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>()
- 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 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 Response.parseAs(): BilibiliResultDto = 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"
- }
-}
diff --git a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliDto.kt b/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliDto.kt
deleted file mode 100644
index fabcda8d4..000000000
--- a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliDto.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package eu.kanade.tachiyomi.extension.zh.bilibilimanga
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class BilibiliResultDto(
- val code: Int = 0,
- val data: T? = null,
- @SerialName("msg") val message: String = "",
-)
-
-@Serializable
-data class BilibiliSearchDto(
- val list: List = emptyList(),
-)
-
-@Serializable
-data class BilibiliComicDto(
- @SerialName("author_name") val authorName: List = emptyList(),
- @SerialName("classic_lines") val classicLines: String = "",
- @SerialName("comic_id") val comicId: Int = 0,
- @SerialName("ep_list") val episodeList: List = 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 = emptyList(),
- val title: String,
- @SerialName("update_weekday") val updateWeekdays: List = 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 = 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? = 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,
-)
diff --git a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliFilters.kt b/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliFilters.kt
deleted file mode 100644
index 42193c01c..000000000
--- a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliFilters.kt
+++ /dev/null
@@ -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(name: String, values: Array, state: Int = 0) :
- Filter.Select(name, values, state) {
-
- val selected: T?
- get() = values.getOrNull(state)
-}
-
-class GenreFilter(label: String, genres: Array) :
- EnhancedSelect(label, genres)
-
-class AreaFilter(label: String, genres: Array) :
- EnhancedSelect(label, genres)
-
-class SortFilter(label: String, options: Array, state: Int = 0) :
- EnhancedSelect(label, options, state)
-
-class StatusFilter(label: String, statuses: Array) :
- Filter.Select(label, statuses)
-
-class PriceFilter(label: String, prices: Array) :
- Filter.Select(label, prices)
diff --git a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliIntl.kt b/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliIntl.kt
deleted file mode 100644
index aedf7db94..000000000
--- a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliIntl.kt
+++ /dev/null
@@ -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 = 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): 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"
- }
-}
diff --git a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliManga.kt b/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliManga.kt
deleted file mode 100644
index 197612a91..000000000
--- a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliManga.kt
+++ /dev/null
@@ -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 {
- val result = response.parseAs()
-
- 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 = arrayOf(
- BilibiliTag(intl.sortPopular, 0),
- BilibiliTag(intl.sortUpdated, 1),
- BilibiliTag(intl.sortFollowers, 2),
- BilibiliTag(intl.sortAdded, 3),
- )
-
- override fun getAllPrices(): Array =
- arrayOf(intl.priceAll, intl.priceFree, intl.pricePaid, intl.priceWaitForFree)
-
- override fun getAllGenres(): Array = 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 = 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"
- }
-}
diff --git a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliUrlActivity.kt b/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliUrlActivity.kt
deleted file mode 100644
index e51e47e85..000000000
--- a/src/zh/bilibilimanga/src/eu/kanade/tachiyomi/extension/zh/bilibilimanga/BilibiliUrlActivity.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/src/zh/kuaikanmanhua/AndroidManifest.xml b/src/zh/kuaikanmanhua/AndroidManifest.xml
deleted file mode 100644
index c88d31966..000000000
--- a/src/zh/kuaikanmanhua/AndroidManifest.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/zh/kuaikanmanhua/build.gradle b/src/zh/kuaikanmanhua/build.gradle
deleted file mode 100644
index b64077b72..000000000
--- a/src/zh/kuaikanmanhua/build.gradle
+++ /dev/null
@@ -1,7 +0,0 @@
-ext {
- extName = 'Kuaikanmanhua'
- extClass = '.Kuaikanmanhua'
- extVersionCode = 10
-}
-
-apply from: "$rootDir/common.gradle"
diff --git a/src/zh/kuaikanmanhua/res/mipmap-hdpi/ic_launcher.png b/src/zh/kuaikanmanhua/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 47365f396..000000000
Binary files a/src/zh/kuaikanmanhua/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/kuaikanmanhua/res/mipmap-mdpi/ic_launcher.png b/src/zh/kuaikanmanhua/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index c2a64f1be..000000000
Binary files a/src/zh/kuaikanmanhua/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/kuaikanmanhua/res/mipmap-xhdpi/ic_launcher.png b/src/zh/kuaikanmanhua/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index ef6a83fdf..000000000
Binary files a/src/zh/kuaikanmanhua/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/kuaikanmanhua/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/kuaikanmanhua/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index d2a6b7eaa..000000000
Binary files a/src/zh/kuaikanmanhua/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/kuaikanmanhua/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/kuaikanmanhua/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 6c0e6a1d2..000000000
Binary files a/src/zh/kuaikanmanhua/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/Kuaikanmanhua.kt b/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/Kuaikanmanhua.kt
deleted file mode 100644
index b0417b2ea..000000000
--- a/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/Kuaikanmanhua.kt
+++ /dev/null
@@ -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 {
- 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 {
- // 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> {
- 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 {
- val data = json.parseToJsonElement(response.body.string())
- .jsonObject["data"]!!
- .jsonObject
- val chaptersJson = data["comics"]!!.jsonArray
- val chapters = mutableListOf()
-
- 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> {
- 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 {
- 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>) :
- Filter.Select(displayName, vals.map { it.first }.toTypedArray()) {
- fun toUriPart() = vals[state].second
- }
-
- companion object {
- const val TOPIC_ID_SEARCH_PREFIX = "topic:"
- }
-}
diff --git a/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/KuaikanmanhuaUrlActivity.kt b/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/KuaikanmanhuaUrlActivity.kt
deleted file mode 100644
index 42d56f8e8..000000000
--- a/src/zh/kuaikanmanhua/src/eu/kanade/tachiyomi/extension/zh/kuaikanmanhua/KuaikanmanhuaUrlActivity.kt
+++ /dev/null
@@ -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)
- }
-}