Comick: hid refactor and add ext-lib 1.4 functions (#15487)

* comick: Refactor the hid changes and add ext-lib 1.4 methods

* bump

* Comick: migrate slugs to hid

* apply suggested changes
This commit is contained in:
mobi2002 2023-02-28 06:26:16 +05:00 committed by GitHub
parent 0c01024f76
commit 1ff1a22fff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 133 deletions

View File

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

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.extension.all.comickfun package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -11,26 +10,23 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
const val API_BASE = "https://api.comick.fun"
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" override val name = "Comick"
override val baseUrl = "https://comick.app" override val baseUrl = "https://comick.app"
private val apiUrl = "https://api.comick.fun"
override val supportsLatest = true override val supportsLatest = true
private val json = Json { private val json = Json {
@ -49,64 +45,32 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
/** Popular Manga **/ /** Popular Manga **/
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET( return searchMangaRequest(
API_BASE.toHttpUrl().newBuilder().apply { page = page,
addPathSegment("v1.0") query = "",
addPathSegment("search") filters = FilterList(
addQueryParameter("sort", "follow") SortFilter("", getSortsList, defaultPopularSort),
addQueryParameter("page", "$page") ),
addQueryParameter("tachiyomi", "true")
}.toString(),
headers,
)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = json.decodeFromString<List<Manga>>(response.body.string())
return MangasPage(
result.map { data ->
SManga.create().apply {
url = "/comic/${data.slug}"
title = data.title
thumbnail_url = data.cover_url
}
},
hasNextPage = true,
) )
} }
override fun popularMangaParse(response: Response) = searchMangaParse(response)
/** Latest Manga **/ /** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return GET( return searchMangaRequest(
API_BASE.toHttpUrl().newBuilder().apply { page = page,
addPathSegment("v1.0") query = "",
addPathSegment("search") filters = FilterList(
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang) SortFilter("", getSortsList, defaultLatestSort),
addQueryParameter("sort", "uploaded") ),
addQueryParameter("page", "$page")
addQueryParameter("tachiyomi", "true")
}.toString(),
headers,
) )
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
val result = json.decodeFromString<List<Manga>>(response.body.string())
return MangasPage(
result.map { data ->
SManga.create().apply {
url = "/comic/${data.slug}"
title = data.title
thumbnail_url = data.cover_url
}
},
hasNextPage = true,
)
}
/** Manga Search **/ /** Manga Search **/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url: String = API_BASE.toHttpUrl().newBuilder().apply { val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("search") addPathSegment("search")
if (query.isEmpty()) { if (query.isEmpty()) {
filters.forEach { it -> filters.forEach { it ->
@ -185,7 +149,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
addQueryParameter("tachiyomi", "true") addQueryParameter("tachiyomi", "true")
addQueryParameter("page", "$page") addQueryParameter("page", "$page")
}.toString() }.build()
return GET(url, headers) return GET(url, headers)
} }
@ -194,7 +158,8 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
return MangasPage( return MangasPage(
result.map { data -> result.map { data ->
SManga.create().apply { SManga.create().apply {
url = "/comic/${data.slug}" // appennding # at end as part of migration from slug to hid
url = "/comic/${data.hid}#"
title = data.title title = data.title
thumbnail_url = data.cover_url thumbnail_url = data.cover_url
} }
@ -204,26 +169,21 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
/** Manga Details **/ /** Manga Details **/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(
GET(
"$API_BASE${manga.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("tachiyomi", "true")
}.toString(),
headers,
),
).asObservableSuccess()
.map { response -> mangaDetailsParse(response).apply { initialized = true } }
}
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl${manga.url}".toHttpUrl().toString()) // Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
val mangaUrl = manga.url.removeSuffix("#")
return GET("$apiUrl$mangaUrl?tachiyomi=true", headers)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val mangaData = json.decodeFromString<MangaDetails>(response.body.string()) val mangaData = json.decodeFromString<MangaDetails>(response.body.string())
return SManga.create().apply { return SManga.create().apply {
url = "$baseUrl/comic/${mangaData.comic.slug}" // appennding # at end as part of migration from slug to hid
url = "/comic/${mangaData.comic.hid}#"
title = mangaData.comic.title title = mangaData.comic.title
artist = mangaData.artists.joinToString { it.name.trim() } artist = mangaData.artists.joinToString { it.name.trim() }
author = mangaData.authors.joinToString { it.name.trim() } author = mangaData.authors.joinToString { it.name.trim() }
@ -234,42 +194,60 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
} }
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl${manga.url}"
}
/** Manga Chapter List **/ /** Manga Chapter List **/
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
// Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
return paginatedChapterListRequest(manga.url.removeSuffix("#"), 1)
}
private fun paginatedChapterListRequest(mangaUrl: String, page: Int): Request {
return GET( return GET(
"$API_BASE${manga.url}".toHttpUrl().newBuilder().apply { "$apiUrl$mangaUrl".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang)
addQueryParameter("tachiyomi", "true") addQueryParameter("tachiyomi", "true")
}.toString(), addQueryParameter("page", "$page")
}.build(),
headers, headers,
) )
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val mangaData = json.decodeFromString<MangaDetails>(response.body.string()) val chapterListResponse = json.decodeFromString<ChapterList>(response.body.string())
val mangaHid = findCurrentSlug(mangaData.comic.slug)
val chapterData = client.newCall( val mangaUrl = "/" + response.request.url.toString()
GET( .substringBefore("/chapters")
API_BASE.toHttpUrl().newBuilder().apply { .substringAfter("$apiUrl/")
addPathSegment("comic")
addPathSegments(mangaHid) var resultSize = chapterListResponse.chapters.size
addPathSegments("chapters") var page = 2
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang)
addQueryParameter( while (chapterListResponse.total > resultSize) {
"limit", val newRequest = paginatedChapterListRequest(mangaUrl, page)
mangaData.comic.chapter_count.toString(), val newResponse = client.newCall(newRequest).execute()
) val newChapterListResponse = json.decodeFromString<ChapterList>(newResponse.body.string())
}.toString(),
headers, chapterListResponse.chapters += newChapterListResponse.chapters
),
).execute() resultSize += newChapterListResponse.chapters.size
val result = json.decodeFromString<ChapterList>(chapterData.body.string()) page += 1
return result.chapters.map { chapter -> }
return chapterListResponse.chapters.map { chapter ->
SChapter.create().apply { SChapter.create().apply {
url = "/comic/${mangaData.comic.slug}/${chapter.hid}-chapter-${chapter.chap}-$comickFunLang" url = "$mangaUrl/${chapter.hid}-chapter-${chapter.chap}-$comickFunLang"
name = beautifyChapterName(chapter.vol, chapter.chap, chapter.title) name = beautifyChapterName(chapter.vol, chapter.chap, chapter.title)
date_upload = chapter.created_at.let { date_upload = chapter.created_at.let {
try { try {
DATE_FORMATTER.parse(it)?.time ?: 0L dateFormat.parse(it)?.time ?: 0L
} catch (e: ParseException) { } catch (e: ParseException) {
0L 0L
} }
@ -279,21 +257,18 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
} }
private val DATE_FORMATTER by lazy { private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
} }
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
/** Chapter Pages **/ /** Chapter Pages **/
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val chapterHid = chapter.url.substringAfterLast("/").substringBefore("-") val chapterHid = chapter.url.substringAfterLast("/").substringBefore("-")
return GET( return GET("$apiUrl/chapter/$chapterHid?tachiyomi=true", headers)
API_BASE.toHttpUrl().newBuilder().apply {
addPathSegment("chapter")
addPathSegment(chapterHid)
addQueryParameter("tachiyomi", "true")
}.toString(),
headers,
)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
@ -307,30 +282,13 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
const val SLUG_SEARCH_PREFIX = "id:" const val SLUG_SEARCH_PREFIX = "id:"
} }
/** Don't touch this, Tachiyomi forces you to declare the following methods even I you don't use them **/
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
return "" throw UnsupportedOperationException("Not used")
} }
protected open val defaultPopularSort: Int = 0
protected open val defaultLatestSort: Int = 4
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
getFilters(), getFilters(),
) )
/** Map the slug to comic ID as slug might be changes by comic ID will not. **/
// TODO: Cleanup once ext-lib 1.4 is released.
private fun findCurrentSlug(oldSlug: String): String {
val response = client.newCall(
GET(
API_BASE.toHttpUrl().newBuilder().apply {
addPathSegment("tachiyomi")
addPathSegment("mapping")
addQueryParameter("slugs", oldSlug)
}.toString(),
headers,
),
).execute()
/** If the API does not contain the ID for the slug, return the slug back **/
return json.parseToJsonElement(response.body.string()).jsonObject[oldSlug]!!.jsonPrimitive.content
}
} }

View File

@ -5,7 +5,6 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Manga( data class Manga(
val hid: String, val hid: String,
val slug: String,
val title: String, val title: String,
val cover_url: String, val cover_url: String,
) )
@ -20,12 +19,10 @@ data class MangaDetails(
@Serializable @Serializable
data class Comic( data class Comic(
val id: Int, val hid: String,
val title: String, val title: String,
val slug: String,
val desc: String = "N/A", val desc: String = "N/A",
val status: Int, val status: Int,
val chapter_count: Int?,
val cover_url: String, val cover_url: String,
) )
@ -49,12 +46,13 @@ data class Genre(
@Serializable @Serializable
data class ChapterList( data class ChapterList(
val chapters: Array<Chapter>, val chapters: MutableList<Chapter>,
val total: Int,
) )
@Serializable @Serializable
data class Chapter( data class Chapter(
val hid: String = "", val hid: String,
val title: String = "", val title: String = "",
val created_at: String = "", val created_at: String = "",
val chap: String = "", val chap: String = "",

View File

@ -45,8 +45,8 @@ internal class FromYearFilter(name: String) : Text(name)
internal class ToYearFilter(name: String) : Text(name) internal class ToYearFilter(name: String) : Text(name)
internal class SortFilter(name: String, sortList: Array<Pair<String, String>>) : internal class SortFilter(name: String, sortList: Array<Pair<String, String>>, state: Int = 0) :
Select(name, sortList) Select(name, sortList, state)
/** Generics **/ /** Generics **/
internal open class Group(name: String, values: List<Any>) : internal open class Group(name: String, values: List<Any>) :
@ -58,8 +58,8 @@ internal open class Text(name: String) : Filter.Text(name)
internal open class CheckBox(name: String, val value: String = "") : Filter.CheckBox(name) internal open class CheckBox(name: String, val value: String = "") : Filter.CheckBox(name)
internal open class Select(name: String, private val vals: Array<Pair<String, String>>) : internal open class Select(name: String, private val vals: Array<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray()) { Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second fun getValue() = vals[state].second
} }
@ -171,7 +171,8 @@ private val getCreatedAtList: Array<Pair<String, String>> = arrayOf(
Pair("1 year", "365"), Pair("1 year", "365"),
) )
private val getSortsList: Array<Pair<String, String>> = arrayOf( internal val getSortsList: Array<Pair<String, String>> = arrayOf(
Pair("Most popular", "follow"),
Pair("Most follows", "user_follow_count"), Pair("Most follows", "user_follow_count"),
Pair("Most views", "view"), Pair("Most views", "view"),
Pair("High rating", "rating"), Pair("High rating", "rating"),