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) - } -}