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:
parent
0c171c680b
commit
ab7e3bcede
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {} }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue