diff --git a/src/en/bilibilicomics/AndroidManifest.xml b/src/en/bilibilicomics/AndroidManifest.xml index afba42cfd..000affda9 100644 --- a/src/en/bilibilicomics/AndroidManifest.xml +++ b/src/en/bilibilicomics/AndroidManifest.xml @@ -15,12 +15,22 @@ + + + + diff --git a/src/en/bilibilicomics/build.gradle b/src/en/bilibilicomics/build.gradle index 1f8bec484..3fb086edc 100644 --- a/src/en/bilibilicomics/build.gradle +++ b/src/en/bilibilicomics/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Bilibili Comics' pkgNameSuffix = 'en.bilibilicomics' extClass = '.BilibiliComics' - extVersionCode = 5 + extVersionCode = 6 libVersion = '1.2' containsNsfw = true } diff --git a/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComics.kt b/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComics.kt index 1bdcea896..516be044a 100644 --- a/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComics.kt +++ b/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComics.kt @@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor import eu.kanade.tachiyomi.network.POST 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 @@ -54,12 +55,19 @@ class BilibiliComics : HttpSource() { private val json: Json by injectLazy() + private val day: Int + get() = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1 + override fun popularMangaRequest(page: Int): Request { val requestPayload = buildJsonObject { - put("id", FEATURED_ID) - put("isAll", 0) - put("page_num", 1) - put("page_size", 6) + put("area_id", -1) + put("is_finish", -1) + put("is_free", 1) + put("order", 0) + put("page_num", page) + put("page_size", POPULAR_PER_PAGE) + put("style_id", -1) + put("style_prefer", "[]") } val requestBody = requestPayload.toString().toRequestBody(JSON_MEDIA_TYPE) @@ -69,35 +77,33 @@ class BilibiliComics : HttpSource() { .build() return POST( - "$baseUrl/$BASE_API_ENDPOINT/GetClassPageSixComics?device=pc&platform=web", + "$baseUrl/$BASE_API_ENDPOINT/ClassPage?device=pc&platform=web", headers = newHeaders, body = requestBody ) } override fun popularMangaParse(response: Response): MangasPage { - val result = json.decodeFromString>(response.body!!.string()) + val result = json.decodeFromString>>(response.body!!.string()) if (result.code != 0) { return MangasPage(emptyList(), hasNextPage = false) } - val comicList = result.data!!.rollSixComics - .map(::popularMangaFromObject) + val comicList = result.data!!.map(::popularMangaFromObject) + val hasNextPage = comicList.size == POPULAR_PER_PAGE - return MangasPage(comicList, hasNextPage = false) + return MangasPage(comicList, hasNextPage) } private fun popularMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply { title = comic.title thumbnail_url = comic.verticalCover - url = "/detail/mc${comic.comicId}" + url = "/detail/mc${comic.seasonId}" } override fun latestUpdatesRequest(page: Int): Request { - val requestPayload = buildJsonObject { - put("day", day) - } + val requestPayload = buildJsonObject { put("day", day) } val requestBody = requestPayload.toString().toRequestBody(JSON_MEDIA_TYPE) val newHeaders = headersBuilder() @@ -131,85 +137,111 @@ class BilibiliComics : HttpSource() { url = "/detail/mc${comic.comicId}" } - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return when { - query.startsWith(prefixIdSearch) -> { - val id = query.removePrefix(prefixIdSearch) - client.newCall(mangaDetailsApiRequestById(id)).asObservableSuccess() - .map { response -> - mangaDetailsParse(response).let { MangasPage(listOf(it), false) } - } - } - - else -> { - client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess() - .map { response -> - searchMangaParse(response) - } - } - } - } - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + if (query.startsWith(PREFIX_ID_SEARCH) && query.matches(ID_SEARCH_PATTERN)) { + val comicId = query + .removePrefix(PREFIX_ID_SEARCH) + .removePrefix("mc") + return mangaDetailsApiRequest("/detail/mc$comicId") + } + + val order = filters.filterIsInstance() + .firstOrNull()?.state ?: 0 + + val status = filters.filterIsInstance() + .firstOrNull()?.state?.minus(1) ?: -1 + + val styleId = filters.filterIsInstance() + .firstOrNull()?.selected?.id ?: -1 + + val pageSize = if (query.isBlank()) POPULAR_PER_PAGE else SEARCH_PER_PAGE + val jsonPayload = buildJsonObject { put("area_id", -1) - put("is_finish", -1) + put("is_finish", status) put("is_free", 1) - put("key_word", query) - put("order", 0) + put("order", order) put("page_num", page) - put("page_size", 9) - put("style_id", -1) + put("page_size", pageSize) + put("style_id", styleId) + 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 = "$baseUrl/search".toHttpUrl().newBuilder() - .addQueryParameter("keyword", query) - .toString() + val refererUrl = if (query.isBlank()) "$baseUrl/genre" else + "$baseUrl/search".toHttpUrl().newBuilder() + .addQueryParameter("keyword", query) + .toString() val newHeaders = headersBuilder() .add("Content-Length", requestBody.contentLength().toString()) .add("Content-Type", requestBody.contentType().toString()) - .add("X-Page", page.toString()) .set("Referer", refererUrl) .build() + val apiPath = if (query.isBlank()) "ClassPage" else "Search" + return POST( - "$baseUrl/$BASE_API_ENDPOINT/Search?device=pc&platform=web", + "$baseUrl/$BASE_API_ENDPOINT/$apiPath?device=pc&platform=web", headers = newHeaders, body = requestBody ) } override fun searchMangaParse(response: Response): MangasPage { + if (response.request.url.toString().contains("ComicDetail")) { + val comic = mangaDetailsParse(response) + return MangasPage(listOf(comic), hasNextPage = false) + } + + if (response.request.url.toString().contains("ClassPage")) { + val result = json.decodeFromString>>(response.body!!.string()) + + 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 = json.decodeFromString>(response.body!!.string()) if (result.code != 0) { return MangasPage(emptyList(), hasNextPage = false) } - val comicList = result.data!!.list - .map(::searchMangaFromObject) + val comicList = result.data!!.list.map(::searchMangaFromObject) + val hasNextPage = comicList.size == SEARCH_PER_PAGE - return MangasPage(comicList, hasNextPage = false) + return MangasPage(comicList, hasNextPage) } private fun searchMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply { title = Jsoup.parse(comic.title).text() thumbnail_url = comic.verticalCover - url = "/detail/mc${comic.id}" + + val comicId = if (comic.id == 0) comic.seasonId else comic.id + url = "/detail/mc$comicId" } // Workaround to allow "Open in browser" use the real URL. override fun fetchMangaDetails(manga: SManga): Observable { - return client.newCall(mangaDetailsApiRequest(manga)) + return client.newCall(mangaDetailsApiRequest(manga.url)) .asObservableSuccess() .map { response -> mangaDetailsParse(response).apply { initialized = true } } } - private fun mangaDetailsApiRequest(manga: SManga): Request { - val comicId = manga.url.substringAfterLast("/mc").toInt() + private fun mangaDetailsApiRequest(mangaUrl: String): Request { + val comicId = mangaUrl.substringAfterLast("/mc").toInt() val jsonPayload = buildJsonObject { put("comic_id", comicId) } val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE) @@ -217,26 +249,7 @@ class BilibiliComics : HttpSource() { val newHeaders = headersBuilder() .add("Content-Length", requestBody.contentLength().toString()) .add("Content-Type", requestBody.contentType().toString()) - .set("Referer", baseUrl + manga.url) - .build() - - return POST( - "$baseUrl/$BASE_API_ENDPOINT/ComicDetail?device=pc&platform=web", - headers = newHeaders, - body = requestBody - ) - } - - private fun mangaDetailsApiRequestById(id: String): Request { - val comicId = id.toInt() - - val jsonPayload = buildJsonObject { put("comic_id", comicId) } - val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE) - - val newHeaders = headersBuilder() - .add("Content-Length", requestBody.contentLength().toString()) - .add("Content-Type", requestBody.contentType().toString()) - .set("Referer", "$baseUrl/detail/mc$id") + .set("Referer", baseUrl + mangaUrl) .build() return POST( @@ -256,10 +269,11 @@ class BilibiliComics : HttpSource() { genre = comic.styles.joinToString() description = comic.classicLines 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 = mangaDetailsApiRequest(manga) + override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga.url) override fun chapterListParse(response: Response): List { val result = json.decodeFromString>(response.body!!.string()) @@ -336,6 +350,47 @@ class BilibiliComics : HttpSource() { return "${page.url}?token=${page.token}" } + private data class Genre(val name: String, val id: Int) { + override fun toString(): String = name + } + + private class GenreFilter(genres: Array) : Filter.Select("Genre", genres) { + val selected: Genre + get() = values[state] + } + + private class SortFilter(options: Array) : Filter.Select("Sort by", options) + private class StatusFilter(statuses: Array) : Filter.Select("Status", statuses) + + private fun getAllGenres(): Array = arrayOf( + Genre("All", -1), + Genre("Action", 19), + Genre("Adventure", 22), + Genre("BL", 3), + Genre("Comedy", 14), + Genre("Eastern", 30), + Genre("Fantasy", 11), + Genre("GL", 16), + Genre("Harem", 15), + Genre("Historical", 12), + Genre("Horror", 23), + Genre("Mistery", 17), + Genre("Romance", 13), + Genre("Slice of Life", 21), + Genre("Suspense", 41), + Genre("Teen", 20) + ) + + private fun getAllSortOptions(): Array = arrayOf("Popular", "Updated") + + private fun getAllStatus(): Array = arrayOf("All", "Ongoing", "Completed") + + override fun getFilterList(): FilterList = FilterList( + StatusFilter(getAllStatus()), + SortFilter(getAllSortOptions()), + GenreFilter(getAllGenres()) + ) + private fun String.toDate(): Long { return try { DATE_FORMATTER.parse(this)?.time ?: 0L @@ -344,19 +399,6 @@ class BilibiliComics : HttpSource() { } } - private val day: Int - get() { - return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { - Calendar.SUNDAY -> 0 - Calendar.MONDAY -> 1 - Calendar.TUESDAY -> 2 - Calendar.WEDNESDAY -> 3 - Calendar.THURSDAY -> 4 - Calendar.FRIDAY -> 5 - else -> 6 - } - } - companion object { private const val BASE_API_ENDPOINT = "twirp/comic.v1.Comic" @@ -364,9 +406,11 @@ class BilibiliComics : HttpSource() { private val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType() - private const val FEATURED_ID = 3 - - const val prefixIdSearch = "id:" + 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 = "^id:(mc)?(\\d+)$".toRegex() private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) } } diff --git a/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComicsUrlActivity.kt b/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComicsUrlActivity.kt index d57d5db71..03e07c713 100644 --- a/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComicsUrlActivity.kt +++ b/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliComicsUrlActivity.kt @@ -20,12 +20,14 @@ class BilibiliComicsUrlActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments if (pathSegments != null && pathSegments.size > 1) { - val titleid = pathSegments[1] + val titleId = pathSegments[1] + val mainIntent = Intent().apply { action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${BilibiliComics.prefixIdSearch}${titleid.removePrefix("mc")}") + putExtra("query", BilibiliComics.PREFIX_ID_SEARCH + titleId) putExtra("filter", packageName) } @@ -35,7 +37,7 @@ class BilibiliComicsUrlActivity : Activity() { Log.e("BilibiliUrlActivity", e.toString()) } } else { - Log.e("BilibiliUrlActivity", "could not parse uri from intent $intent") + Log.e("BilibiliUrlActivity", "Could not parse URI from intent $intent") } finish() diff --git a/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliDto.kt b/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliDto.kt index f8e6d934d..11bf5cb6f 100644 --- a/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliDto.kt +++ b/src/en/bilibilicomics/src/eu/kanade/tachiyomi/extension/en/bilibilicomics/BilibiliDto.kt @@ -33,6 +33,7 @@ data class BilibiliComicDto( @SerialName("ep_list") val episodeList: List = emptyList(), val id: Int = 0, @SerialName("is_finish") val isFinish: Int = 0, + @SerialName("season_id") val seasonId: Int = 0, val styles: List = emptyList(), val title: String, @SerialName("vertical_cover") val verticalCover: String = ""