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'
pkgNameSuffix = 'all.comickfun'
extClass = '.ComickFunFactory'
extVersionCode = 6
extVersionCode = 7
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() {
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<List<SManga>>("data", tDeserializer = ListSerializer(deepSelectDeserializer("md_comics"))),
return json.decodeFromString<List<SManga>>(
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<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()
)
/** 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<List<SChapter>> {
@ -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<List<SChapter>>("data", "chapters"),
deserializer = deepSelectDeserializer<List<SChapter>>("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<List<String>>("data", "chapter", "images"),
deserializer = deepSelectDeserializer<List<String>>("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<LabeledValue>
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<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(
Filter.Header("NOTE: Ignored if using text search!"),
GenreFilter(),
DemographicFilter(),
TypesFilter(),
CreatedAtFilter(),
MinChaptersFilter()
MinChaptersFilter(),
SortFilter()
)
private fun GenreFilter() = object : MultiSelect<LabeledValue>("Genre", getGenreList()), ArrayUrlParam {
override val paramName = "genres"
private fun GenreFilter() = object : MultiTriSelect<LabeledValue>("Genre", getGenreList()), UrlEncoded {
val included = object : ArrayUrlParam {
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 {
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 {
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:"
}

View File

@ -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<Source> = 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) {} }
}

View File

@ -193,7 +193,7 @@ class SMangaDeserializer : KSerializer<SManga> {
override val descriptor = buildClassSerialDescriptor(SManga::class.qualifiedName!!) {
element<String>("slug")
element<String>("title")
element<String>("coverURL")
element<String>("cover_url")
element<String>("id", isOptional = true)
element<List<JsonObject>>("artists", isOptional = true)
element<List<JsonObject>>("authors", isOptional = true)
@ -233,7 +233,7 @@ class SMangaDeserializer : KSerializer<SManga> {
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()