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:
parent
0c01024f76
commit
1ff1a22fff
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'Comick'
|
||||
pkgNameSuffix = 'all.comickfun'
|
||||
extClass = '.ComickFunFactory'
|
||||
extVersionCode = 20
|
||||
extVersionCode = 21
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
const val API_BASE = "https://api.comick.fun"
|
||||
|
||||
abstract class ComickFun(override val lang: String, private val comickFunLang: String) : HttpSource() {
|
||||
|
||||
override val name = "Comick"
|
||||
|
||||
override val baseUrl = "https://comick.app"
|
||||
|
||||
private val apiUrl = "https://api.comick.fun"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json = Json {
|
||||
@ -49,64 +45,32 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||
|
||||
/** Popular Manga **/
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET(
|
||||
API_BASE.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("v1.0")
|
||||
addPathSegment("search")
|
||||
addQueryParameter("sort", "follow")
|
||||
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,
|
||||
return searchMangaRequest(
|
||||
page = page,
|
||||
query = "",
|
||||
filters = FilterList(
|
||||
SortFilter("", getSortsList, defaultPopularSort),
|
||||
),
|
||||
)
|
||||
}
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
/** Latest Manga **/
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET(
|
||||
API_BASE.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("v1.0")
|
||||
addPathSegment("search")
|
||||
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang)
|
||||
addQueryParameter("sort", "uploaded")
|
||||
addQueryParameter("page", "$page")
|
||||
addQueryParameter("tachiyomi", "true")
|
||||
}.toString(),
|
||||
headers,
|
||||
return searchMangaRequest(
|
||||
page = page,
|
||||
query = "",
|
||||
filters = FilterList(
|
||||
SortFilter("", getSortsList, defaultLatestSort),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(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 latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
/** Manga Search **/
|
||||
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")
|
||||
if (query.isEmpty()) {
|
||||
filters.forEach { it ->
|
||||
@ -185,7 +149,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||
}
|
||||
addQueryParameter("tachiyomi", "true")
|
||||
addQueryParameter("page", "$page")
|
||||
}.toString()
|
||||
}.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
@ -194,7 +158,8 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||
return MangasPage(
|
||||
result.map { data ->
|
||||
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
|
||||
thumbnail_url = data.cover_url
|
||||
}
|
||||
@ -204,26 +169,21 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
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 {
|
||||
val mangaData = json.decodeFromString<MangaDetails>(response.body.string())
|
||||
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
|
||||
artist = mangaData.artists.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 **/
|
||||
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(
|
||||
"$API_BASE${manga.url}".toHttpUrl().newBuilder().apply {
|
||||
"$apiUrl$mangaUrl".toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("chapters")
|
||||
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang)
|
||||
addQueryParameter("tachiyomi", "true")
|
||||
}.toString(),
|
||||
addQueryParameter("page", "$page")
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val mangaData = json.decodeFromString<MangaDetails>(response.body.string())
|
||||
val mangaHid = findCurrentSlug(mangaData.comic.slug)
|
||||
val chapterData = client.newCall(
|
||||
GET(
|
||||
API_BASE.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("comic")
|
||||
addPathSegments(mangaHid)
|
||||
addPathSegments("chapters")
|
||||
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang)
|
||||
addQueryParameter(
|
||||
"limit",
|
||||
mangaData.comic.chapter_count.toString(),
|
||||
)
|
||||
}.toString(),
|
||||
headers,
|
||||
),
|
||||
).execute()
|
||||
val result = json.decodeFromString<ChapterList>(chapterData.body.string())
|
||||
return result.chapters.map { chapter ->
|
||||
val chapterListResponse = json.decodeFromString<ChapterList>(response.body.string())
|
||||
|
||||
val mangaUrl = "/" + response.request.url.toString()
|
||||
.substringBefore("/chapters")
|
||||
.substringAfter("$apiUrl/")
|
||||
|
||||
var resultSize = chapterListResponse.chapters.size
|
||||
var page = 2
|
||||
|
||||
while (chapterListResponse.total > resultSize) {
|
||||
val newRequest = paginatedChapterListRequest(mangaUrl, page)
|
||||
val newResponse = client.newCall(newRequest).execute()
|
||||
val newChapterListResponse = json.decodeFromString<ChapterList>(newResponse.body.string())
|
||||
|
||||
chapterListResponse.chapters += newChapterListResponse.chapters
|
||||
|
||||
resultSize += newChapterListResponse.chapters.size
|
||||
page += 1
|
||||
}
|
||||
|
||||
return chapterListResponse.chapters.map { chapter ->
|
||||
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)
|
||||
date_upload = chapter.created_at.let {
|
||||
try {
|
||||
DATE_FORMATTER.parse(it)?.time ?: 0L
|
||||
dateFormat.parse(it)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return "$baseUrl${chapter.url}"
|
||||
}
|
||||
|
||||
/** Chapter Pages **/
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterHid = chapter.url.substringAfterLast("/").substringBefore("-")
|
||||
return GET(
|
||||
API_BASE.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("chapter")
|
||||
addPathSegment(chapterHid)
|
||||
addQueryParameter("tachiyomi", "true")
|
||||
}.toString(),
|
||||
headers,
|
||||
)
|
||||
return GET("$apiUrl/chapter/$chapterHid?tachiyomi=true", headers)
|
||||
}
|
||||
|
||||
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:"
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
return ""
|
||||
throw UnsupportedOperationException("Not used")
|
||||
}
|
||||
|
||||
protected open val defaultPopularSort: Int = 0
|
||||
protected open val defaultLatestSort: Int = 4
|
||||
override fun getFilterList() = FilterList(
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class Manga(
|
||||
val hid: String,
|
||||
val slug: String,
|
||||
val title: String,
|
||||
val cover_url: String,
|
||||
)
|
||||
@ -20,12 +19,10 @@ data class MangaDetails(
|
||||
|
||||
@Serializable
|
||||
data class Comic(
|
||||
val id: Int,
|
||||
val hid: String,
|
||||
val title: String,
|
||||
val slug: String,
|
||||
val desc: String = "N/A",
|
||||
val status: Int,
|
||||
val chapter_count: Int?,
|
||||
val cover_url: String,
|
||||
)
|
||||
|
||||
@ -49,12 +46,13 @@ data class Genre(
|
||||
|
||||
@Serializable
|
||||
data class ChapterList(
|
||||
val chapters: Array<Chapter>,
|
||||
val chapters: MutableList<Chapter>,
|
||||
val total: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Chapter(
|
||||
val hid: String = "",
|
||||
val hid: String,
|
||||
val title: String = "",
|
||||
val created_at: String = "",
|
||||
val chap: String = "",
|
||||
|
@ -45,8 +45,8 @@ internal class FromYearFilter(name: String) : Text(name)
|
||||
|
||||
internal class ToYearFilter(name: String) : Text(name)
|
||||
|
||||
internal class SortFilter(name: String, sortList: Array<Pair<String, String>>) :
|
||||
Select(name, sortList)
|
||||
internal class SortFilter(name: String, sortList: Array<Pair<String, String>>, state: Int = 0) :
|
||||
Select(name, sortList, state)
|
||||
|
||||
/** Generics **/
|
||||
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 Select(name: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray()) {
|
||||
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(), state) {
|
||||
fun getValue() = vals[state].second
|
||||
}
|
||||
|
||||
@ -171,7 +171,8 @@ private val getCreatedAtList: Array<Pair<String, String>> = arrayOf(
|
||||
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 views", "view"),
|
||||
Pair("High rating", "rating"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user