Comick.fun: Migrate to new API (#9280)

* Migrate to new api endpoint + Implement Required Changes

* Update endpoints and response parsing to reflect API changes

* Restore search/filter functionality

* Add genre exclusion and search result sorting

* Comick.fun now specifies language with ISO639 compliant language codes instead of country codes
This commit is contained in:
h-hyuuga 2021-10-13 08:19:58 -04:00 committed by GitHub
parent 0c171c680b
commit ab7e3bcede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 71 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Comick.fun' extName = 'Comick.fun'
pkgNameSuffix = 'all.comickfun' pkgNameSuffix = 'all.comickfun'
extClass = '.ComickFunFactory' extClass = '.ComickFunFactory'
extVersionCode = 6 extVersionCode = 7
isNsfw = true isNsfw = true
} }

View File

@ -36,7 +36,7 @@ const val SEARCH_PAGE_LIMIT = 100
abstract class ComickFun(override val lang: String, private val comickFunLang: String) : HttpSource() { abstract class ComickFun(override val lang: String, private val comickFunLang: String) : HttpSource() {
override val name = "Comick.fun" override val name = "Comick.fun"
final override val baseUrl = "https://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 override val supportsLatest = true
@ExperimentalSerializationApi @ExperimentalSerializationApi
@ -66,14 +66,25 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
builder.addInterceptor( builder.addInterceptor(
Interceptor { chain -> Interceptor { chain ->
val request = chain.request() val request = chain.request()
val path = request.url.pathSegments
when { 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()) chain.proceed(request.newBuilder().url(request.url.newBuilder().addQueryParameter("lang", comickFunLang).build()).build())
else -> chain.proceed(request) 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 Limiter, shamelessly ~stolen from~ inspired by MangaDex
* Rate limits all requests that go to the baseurl * 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) val noResults = MangasPage(emptyList(), false)
if (response.code == 204) if (response.code == 204)
return noResults return noResults
return json.decodeFromString( return json.decodeFromString<List<SManga>>(
deserializer = deepSelectDeserializer<List<SManga>>("data", tDeserializer = ListSerializer(deepSelectDeserializer("md_comics"))), deserializer = ListSerializer(deepSelectDeserializer("md_comics")),
response.body!!.string() response.body!!.string()
).let { MangasPage(it, true) } ).let { MangasPage(it, true) }
} }
override fun latestUpdatesRequest(page: Int): Request { 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("page", "${page - 1}")
.addQueryParameter("device-memory", "8") .addQueryParameter("device-memory", "8")
return GET("$url", headers) 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 { 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()) { if (query.isNotEmpty()) {
url.addPathSegment("search_title") url.addQueryParameter("q", query)
.addQueryParameter("t", "1")
.addQueryParameter("q", query)
} else { } else {
url.addPathSegment("search") url.addQueryParameter("page", "$page")
.addQueryParameter("page", "$page")
.addQueryParameter("limit", "$SEARCH_PAGE_LIMIT") .addQueryParameter("limit", "$SEARCH_PAGE_LIMIT")
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
@ -174,7 +182,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
/** Manga Details **/ /** Manga Details **/
private fun apiMangaDetailsRequest(manga: SManga): Request { 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 // 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 @ExperimentalSerializationApi
override fun mangaDetailsParse(response: Response) = json.decodeFromString( override fun mangaDetailsParse(response: Response) = json.decodeFromString(
deserializer = deepSelectDeserializer<SManga>("data", tDeserializer = jsonFlatten(objKey = "comic", "id", "title", "desc", "status", "country", "slug")), deserializer = jsonFlatten<SManga>(objKey = "comic", "id", "title", "desc", "status", "country", "slug"),
response.body!!.string() response.body!!.string()
) )
/** Chapter List **/ /** Chapter List **/
private fun chapterListRequest(page: Int, mangaId: Int) = 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 @ExperimentalSerializationApi
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
@ -225,18 +233,18 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
@ExperimentalSerializationApi @ExperimentalSerializationApi
override fun chapterListParse(response: Response) = json.decodeFromString( override fun chapterListParse(response: Response) = json.decodeFromString(
deserializer = deepSelectDeserializer<List<SChapter>>("data", "chapters"), deserializer = deepSelectDeserializer<List<SChapter>>("chapters"),
response.body!!.string() response.body!!.string()
) )
/** Page List **/ /** 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 @ExperimentalSerializationApi
override fun pageListParse(response: Response) = override fun pageListParse(response: Response) =
json.decodeFromString( json.decodeFromString(
deserializer = deepSelectDeserializer<List<String>>("data", "chapter", "images"), deserializer = deepSelectDeserializer<List<String>>("chapter", "images", tDeserializer = ListSerializer(deepSelectDeserializer("url"))),
response.body!!.string() response.body!!.string()
).mapIndexed { i, url -> Page(i, imageUrl = url) } ).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 paramName: String
val selected: Sequence<LabeledValue> val selected: Sequence<LabeledValue>
override fun encode(url: HttpUrl.Builder) { 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 } get() = this.elems.asSequence().filterIndexed { i, _ -> this.state[i].state }
} }
private open class MultiTriSelect<T>(header: String, val elems: List<T>) :
Filter.Group<Filter.TriState>(header, elems.map { object : Filter.TriState("$it") {} }) {
val selected: Pair<Sequence<T>, Sequence<T>>
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( override fun getFilterList() = FilterList(
Filter.Header("NOTE: Ignored if using text search!"), Filter.Header("NOTE: Ignored if using text search!"),
GenreFilter(), GenreFilter(),
DemographicFilter(), DemographicFilter(),
TypesFilter(), TypesFilter(),
CreatedAtFilter(), CreatedAtFilter(),
MinChaptersFilter() MinChaptersFilter(),
SortFilter()
) )
private fun GenreFilter() = object : MultiSelect<LabeledValue>("Genre", getGenreList()), ArrayUrlParam { private fun GenreFilter() = object : MultiTriSelect<LabeledValue>("Genre", getGenreList()), UrlEncoded {
val included = object : ArrayUrlParam {
override val paramName = "genres" override val paramName = "genres"
override var selected: Sequence<LabeledValue> = sequence {}
}
val excluded = object : ArrayUrlParam {
override val paramName = "excludes"
override var selected: Sequence<LabeledValue> = 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<LabeledValue>("Sort", getSorts()), QueryParam {
override val paramName = "sort"
}
private fun DemographicFilter() = object : MultiSelect<LabeledValue>("Demographic", getDemographics()), ArrayUrlParam { private fun DemographicFilter() = object : MultiSelect<LabeledValue>("Demographic", getDemographics()), ArrayUrlParam {
override val paramName = "demographic" override val paramName = "demographic"
} }
@ -304,6 +344,10 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
private fun CreatedAtFilter() = object : Select<LabeledValue>("Created At", getCreatedAt()), QueryParam { private fun CreatedAtFilter() = object : Select<LabeledValue>("Created At", getCreatedAt()), QueryParam {
override val paramName = "time" 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 { 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"), 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 { companion object {
const val SLUG_SEARCH_PREFIX = "id:" const val SLUG_SEARCH_PREFIX = "id:"
} }

View File

@ -3,73 +3,55 @@ package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
val toISO639 = mapOf( // A legacy mapping of language codes to ensure that source IDs don't change
"gb" to "en", // English val legacyLanguageMappings = mapOf(
"br" to "pt-BR", // Brazilian Portuguese "pt-br" to "pt-BR", // Brazilian Portuguese
"mx" to "es-419", // Latin-American Spanish "zh-hk" to "zh-Hant", // Traditional Chinese,
"vn" to "vi", // Vietnemese "zh" to "zh-Hans", // Simplified Chinese
"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
).withDefault { it } // country code matches language code ).withDefault { it } // country code matches language code
class ComickFunFactory : SourceFactory { class ComickFunFactory : SourceFactory {
override fun createSources(): List<Source> = listOf( override fun createSources(): List<Source> = listOf(
"all", "all",
"gb", "en",
"br", "pt-br",
"ru", "ru",
"fr", "fr",
"mx", "es-419",
"pl", "pl",
"tr", "tr",
"it", "it",
"es", "es",
"id", "id",
"hu", "hu",
"vn", "vi",
"hk", "zh-hk",
"sa", "ar",
"de", "de",
"cn", "zh",
"ct", "ca",
"bg", "bg",
"th", "th",
"ir", "fa",
"ua", "uk",
"mn", "mn",
"ro", "ro",
"il", "he",
"ms",
"tl",
"ja",
"hi",
"my", "my",
"ph", "ko",
"jp", "cs",
"in",
"mm",
"kr",
"cz",
"pt", "pt",
"nl", "nl",
"se", "sv",
"bd", "bn",
"no", "no",
"lt", "lt",
"gr", "el",
"rs", "sr",
"dk" "da"
).map { object : ComickFun(toISO639.getValue(it), it) {} } ).map { object : ComickFun(legacyLanguageMappings.getValue(it), it) {} }
} }

View File

@ -193,7 +193,7 @@ class SMangaDeserializer : KSerializer<SManga> {
override val descriptor = buildClassSerialDescriptor(SManga::class.qualifiedName!!) { override val descriptor = buildClassSerialDescriptor(SManga::class.qualifiedName!!) {
element<String>("slug") element<String>("slug")
element<String>("title") element<String>("title")
element<String>("coverURL") element<String>("cover_url")
element<String>("id", isOptional = true) element<String>("id", isOptional = true)
element<List<JsonObject>>("artists", isOptional = true) element<List<JsonObject>>("artists", isOptional = true)
element<List<JsonObject>>("authors", isOptional = true) element<List<JsonObject>>("authors", isOptional = true)
@ -233,7 +233,7 @@ class SMangaDeserializer : KSerializer<SManga> {
url = "/comic/$slug" url = "/comic/$slug"
} }
"title" -> title = decodeStringElement(descriptor, index) "title" -> title = decodeStringElement(descriptor, index)
"coverURL" -> thumbnail_url = decodeStringElement(descriptor, index) "cover_url" -> thumbnail_url = decodeStringElement(descriptor, index)
"id" -> id = decodeIntElement(descriptor, index) "id" -> id = decodeIntElement(descriptor, index)
"artists" -> artist = nameList() "artists" -> artist = nameList()
"authors" -> author = nameList() "authors" -> author = nameList()