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'
|
extName = 'Comick'
|
||||||
pkgNameSuffix = 'all.comickfun'
|
pkgNameSuffix = 'all.comickfun'
|
||||||
extClass = '.ComickFunFactory'
|
extClass = '.ComickFunFactory'
|
||||||
extVersionCode = 20
|
extVersionCode = 21
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 = "",
|
||||||
|
@ -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"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user