diff --git a/src/all/comickfun/build.gradle b/src/all/comickfun/build.gradle index 86c04769e..3e3ba0625 100644 --- a/src/all/comickfun/build.gradle +++ b/src/all/comickfun/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Comick.fun' pkgNameSuffix = 'all.comickfun' extClass = '.ComickFunFactory' - extVersionCode = 6 + extVersionCode = 7 isNsfw = true } diff --git a/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFun.kt b/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFun.kt index 16aa4c3c0..3486f61e8 100644 --- a/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFun.kt +++ b/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFun.kt @@ -36,7 +36,7 @@ const val SEARCH_PAGE_LIMIT = 100 abstract class ComickFun(override val lang: String, private val comickFunLang: String) : HttpSource() { override val name = "Comick.fun" final override val baseUrl = "https://comick.fun" - private val apiBase = "$baseUrl/api" + private val apiBase = "https://api.comick.fun" override val supportsLatest = true @ExperimentalSerializationApi @@ -66,14 +66,25 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S builder.addInterceptor( Interceptor { chain -> val request = chain.request() + val path = request.url.pathSegments when { - request.url.toString().contains(Regex("""$apiBase/(?:get_chapters|get_newest_chapters)""")) -> + ((path.size == 1) && (path[0] == "chapter")) || + ((path.size == 3) && (path[0] == "comic") && (path[2] == "chapter")) -> chain.proceed(request.newBuilder().url(request.url.newBuilder().addQueryParameter("lang", comickFunLang).build()).build()) else -> chain.proceed(request) } } ) - + // Add interceptor to append "tachiyomi=true" to all requests (api returns slightly different response to 3rd parties) + builder.addInterceptor( + Interceptor { chain -> + val request = chain.request() + return@Interceptor when (request.url.toString().startsWith(apiBase)) { + true -> chain.proceed(request.newBuilder().url(request.url.newBuilder().addQueryParameter("tachiyomi", "true").build()).build()) + false -> chain.proceed(request) + } + } + ) /** Rate Limiter, shamelessly ~stolen from~ inspired by MangaDex * Rate limits all requests that go to the baseurl */ @@ -117,14 +128,14 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S val noResults = MangasPage(emptyList(), false) if (response.code == 204) return noResults - return json.decodeFromString( - deserializer = deepSelectDeserializer>("data", tDeserializer = ListSerializer(deepSelectDeserializer("md_comics"))), + return json.decodeFromString>( + deserializer = ListSerializer(deepSelectDeserializer("md_comics")), response.body!!.string() ).let { MangasPage(it, true) } } override fun latestUpdatesRequest(page: Int): Request { - val url = "$apiBase/get_newest_chapters".toHttpUrl().newBuilder() + val url = "$apiBase/chapter".toHttpUrl().newBuilder() .addQueryParameter("page", "${page - 1}") .addQueryParameter("device-memory", "8") return GET("$url", headers) @@ -143,14 +154,11 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = apiBase.toHttpUrl().newBuilder() + val url = apiBase.toHttpUrl().newBuilder().addPathSegment("search") if (query.isNotEmpty()) { - url.addPathSegment("search_title") - .addQueryParameter("t", "1") - .addQueryParameter("q", query) + url.addQueryParameter("q", query) } else { - url.addPathSegment("search") - .addQueryParameter("page", "$page") + url.addQueryParameter("page", "$page") .addQueryParameter("limit", "$SEARCH_PAGE_LIMIT") filters.forEach { filter -> when (filter) { @@ -174,7 +182,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S /** Manga Details **/ private fun apiMangaDetailsRequest(manga: SManga): Request { - return GET("$apiBase/get_comic?slug=${slug(manga)}", headers) + return GET("$apiBase/comic/${slug(manga)}", headers) } // Shenanigans to allow "open in webview" to show a webpage instead of JSON @@ -189,14 +197,14 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S @ExperimentalSerializationApi override fun mangaDetailsParse(response: Response) = json.decodeFromString( - deserializer = deepSelectDeserializer("data", tDeserializer = jsonFlatten(objKey = "comic", "id", "title", "desc", "status", "country", "slug")), + deserializer = jsonFlatten(objKey = "comic", "id", "title", "desc", "status", "country", "slug"), response.body!!.string() ) /** Chapter List **/ private fun chapterListRequest(page: Int, mangaId: Int) = - GET("$apiBase/get_chapters?comicid=$mangaId&page=$page&limit=$SEARCH_PAGE_LIMIT", headers) + GET("$apiBase/comic/$mangaId/chapter?page=$page&limit=$SEARCH_PAGE_LIMIT", headers) @ExperimentalSerializationApi override fun fetchChapterList(manga: SManga): Observable> { @@ -225,18 +233,18 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S @ExperimentalSerializationApi override fun chapterListParse(response: Response) = json.decodeFromString( - deserializer = deepSelectDeserializer>("data", "chapters"), + deserializer = deepSelectDeserializer>("chapters"), response.body!!.string() ) /** Page List **/ - override fun pageListRequest(chapter: SChapter) = GET("$apiBase/get_chapter?hid=${hid(chapter)}", headers, CacheControl.FORCE_NETWORK) + override fun pageListRequest(chapter: SChapter) = GET("$apiBase/chapter/${hid(chapter)}", headers, CacheControl.FORCE_NETWORK) @ExperimentalSerializationApi override fun pageListParse(response: Response) = json.decodeFromString( - deserializer = deepSelectDeserializer>("data", "chapter", "images"), + deserializer = deepSelectDeserializer>("chapter", "images", tDeserializer = ListSerializer(deepSelectDeserializer("url"))), response.body!!.string() ).mapIndexed { i, url -> Page(i, imageUrl = url) } @@ -252,7 +260,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S val paramName: String val selected: Sequence override fun encode(url: HttpUrl.Builder) { - url.addQueryParameter(paramName, selected.joinToString(",") { it.value }) + selected.forEach { url.addQueryParameter(paramName, it.value) } } } @@ -281,19 +289,51 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S get() = this.elems.asSequence().filterIndexed { i, _ -> this.state[i].state } } + private open class MultiTriSelect(header: String, val elems: List) : + Filter.Group(header, elems.map { object : Filter.TriState("$it") {} }) { + val selected: Pair, Sequence> + get() { + return this.elems.asSequence() + .mapIndexed { index, it -> index to it } + .filterNot { (index, _) -> this.state[index].isIgnored() } + .partition { (index, _) -> this.state[index].isIncluded() } + .let { (included, excluded) -> + included.asSequence().map { it.second } to excluded.asSequence().map { it.second } + } + } + } + override fun getFilterList() = FilterList( Filter.Header("NOTE: Ignored if using text search!"), GenreFilter(), DemographicFilter(), TypesFilter(), CreatedAtFilter(), - MinChaptersFilter() + MinChaptersFilter(), + SortFilter() ) - private fun GenreFilter() = object : MultiSelect("Genre", getGenreList()), ArrayUrlParam { - override val paramName = "genres" + private fun GenreFilter() = object : MultiTriSelect("Genre", getGenreList()), UrlEncoded { + val included = object : ArrayUrlParam { + override val paramName = "genres" + override var selected: Sequence = sequence {} + } + val excluded = object : ArrayUrlParam { + override val paramName = "excludes" + override var selected: Sequence = sequence {} + } + + override fun encode(url: HttpUrl.Builder) { + this.selected.let { (includedGenres, excludedGenres) -> + included.apply { selected = includedGenres }.encode(url) + excluded.apply { selected = excludedGenres }.encode(url) + } + } } + private fun SortFilter() = object : Select("Sort", getSorts()), QueryParam { + override val paramName = "sort" + } private fun DemographicFilter() = object : MultiSelect("Demographic", getDemographics()), ArrayUrlParam { override val paramName = "demographic" } @@ -304,6 +344,10 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S private fun CreatedAtFilter() = object : Select("Created At", getCreatedAt()), QueryParam { override val paramName = "time" + override fun encode(url: HttpUrl.Builder) { + // api will reject a request with an empty time + if (selected.value.isNotBlank()) super.encode(url) + } } private fun MinChaptersFilter() = object : Filter.Text("Minimum Chapters", ""), UrlEncoded { @@ -423,6 +467,14 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S LabeledValue("1 year", "365"), ) + private fun getSorts() = arrayOf( + LabeledValue("", ""), + LabeledValue("Most follows", "follow"), + LabeledValue("Most views", "view"), + LabeledValue("High rating", "rating"), + LabeledValue("Last updated", "uploaded") + ) + companion object { const val SLUG_SEARCH_PREFIX = "id:" } diff --git a/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunFactory.kt b/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunFactory.kt index 2f3fdace2..a60d02778 100644 --- a/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunFactory.kt +++ b/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunFactory.kt @@ -3,73 +3,55 @@ package eu.kanade.tachiyomi.extension.all.comickfun import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory -val toISO639 = mapOf( - "gb" to "en", // English - "br" to "pt-BR", // Brazilian Portuguese - "mx" to "es-419", // Latin-American Spanish - "vn" to "vi", // Vietnemese - "hk" to "zh-Hant", // Traditional Chinese, - "cn" to "zh-Hans", // Simplified Chinese - "sa" to "ar", // Arabic - "ct" to "ca", // Catalan; Valencian - "ir" to "fa", // Persian - "ua" to "uk", // Ukranian - "il" to "he", // hebrew - "my" to "ms", // Malay - "ph" to "tl", // Filipino - "jp" to "ja", // Japanese - "in" to "hi", // Hindi - "kr" to "ko", // Korean - "cz" to "cs", // Czech - "bd" to "bn", // Bengali - "gr" to "el", // Modern Greek - "rs" to "sr", // Serbo-Croatian - "dk" to "da", // Danish - +// A legacy mapping of language codes to ensure that source IDs don't change +val legacyLanguageMappings = mapOf( + "pt-br" to "pt-BR", // Brazilian Portuguese + "zh-hk" to "zh-Hant", // Traditional Chinese, + "zh" to "zh-Hans", // Simplified Chinese ).withDefault { it } // country code matches language code class ComickFunFactory : SourceFactory { override fun createSources(): List = listOf( "all", - "gb", - "br", + "en", + "pt-br", "ru", "fr", - "mx", + "es-419", "pl", "tr", "it", "es", "id", "hu", - "vn", - "hk", - "sa", + "vi", + "zh-hk", + "ar", "de", - "cn", - "ct", + "zh", + "ca", "bg", "th", - "ir", - "ua", + "fa", + "uk", "mn", "ro", - "il", + "he", + "ms", + "tl", + "ja", + "hi", "my", - "ph", - "jp", - "in", - "mm", - "kr", - "cz", + "ko", + "cs", "pt", "nl", - "se", - "bd", + "sv", + "bn", "no", "lt", - "gr", - "rs", - "dk" - ).map { object : ComickFun(toISO639.getValue(it), it) {} } + "el", + "sr", + "da" + ).map { object : ComickFun(legacyLanguageMappings.getValue(it), it) {} } } diff --git a/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunSerialization.kt b/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunSerialization.kt index aa089b3ae..00dadb5e4 100644 --- a/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunSerialization.kt +++ b/src/all/comickfun/src/eu/kanade/tachiyomi/extension/all/comickfun/ComickFunSerialization.kt @@ -193,7 +193,7 @@ class SMangaDeserializer : KSerializer { override val descriptor = buildClassSerialDescriptor(SManga::class.qualifiedName!!) { element("slug") element("title") - element("coverURL") + element("cover_url") element("id", isOptional = true) element>("artists", isOptional = true) element>("authors", isOptional = true) @@ -233,7 +233,7 @@ class SMangaDeserializer : KSerializer { url = "/comic/$slug" } "title" -> title = decodeStringElement(descriptor, index) - "coverURL" -> thumbnail_url = decodeStringElement(descriptor, index) + "cover_url" -> thumbnail_url = decodeStringElement(descriptor, index) "id" -> id = decodeIntElement(descriptor, index) "artists" -> artist = nameList() "authors" -> author = nameList()