AllAnime refactor (#16660)
* AllAnime refactor * code cleanup * chapter titles\dates * image quality setting * [skip ci] add more warning
This commit is contained in:
parent
1b47daff10
commit
6aa5ac0f17
|
@ -6,7 +6,7 @@ ext {
|
|||
extName = 'AllAnime'
|
||||
pkgNameSuffix = 'en.allanime'
|
||||
extClass = '.AllAnime'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -10,8 +10,6 @@ import eu.kanade.tachiyomi.network.POST
|
|||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
|
||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -20,24 +18,18 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class AllAnime : ConfigurableSource, HttpSource() {
|
||||
|
@ -48,18 +40,41 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val json: Json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
encodeDefaults = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
||||
private val domain = preferences.getString(DOMAIN_PREF, "allanime.to")
|
||||
private val domain = preferences.domainPref
|
||||
|
||||
override val baseUrl = "https://$domain"
|
||||
|
||||
private val apiUrl = "https://api.$domain/allanimeapi"
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val frag = request.url.fragment
|
||||
val quality = preferences.imageQuality
|
||||
|
||||
if (frag.isNullOrEmpty() || quality == IMAGE_QUALITY_PREF_DEFAULT) {
|
||||
return@addInterceptor chain.proceed(request)
|
||||
}
|
||||
|
||||
val oldUrl = request.url.toString()
|
||||
val newUrl = oldUrl.replace(imageQualityRegex, "$image_cdn/$1?w=$quality")
|
||||
|
||||
return@addInterceptor chain.proceed(
|
||||
request.newBuilder()
|
||||
.url(newUrl)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
.rateLimitHost(apiUrl.toHttpUrl(), 1)
|
||||
.build()
|
||||
|
||||
|
@ -68,31 +83,22 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
|
||||
/* Popular */
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val showAdult = preferences.getBoolean(SHOW_ADULT_PREF, false)
|
||||
val payloadObj = ApiPopularPayload(
|
||||
size = limit,
|
||||
dateRange = 0,
|
||||
page = page,
|
||||
allowAdult = preferences.allowAdult,
|
||||
)
|
||||
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("type", "manga")
|
||||
put("size", limit)
|
||||
put("dateRange", 0)
|
||||
put("page", page)
|
||||
put("allowAdult", showAdult)
|
||||
put("allowUnknown", false)
|
||||
}
|
||||
put("query", POPULAR_QUERY)
|
||||
}
|
||||
|
||||
return apiRequest(payload)
|
||||
return apiRequest(payloadObj)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = json.decodeFromString<ApiPopularResponse>(response.body.string())
|
||||
val result = response.parseAs<ApiPopularResponse>()
|
||||
val titleStyle = preferences.titlePref
|
||||
|
||||
val mangaList = result.data.queryPopular.recommendations
|
||||
.mapNotNull { it.anyCard }
|
||||
.map { manga ->
|
||||
toSManga(manga)
|
||||
}
|
||||
val mangaList = result.data.popular.mangas
|
||||
.mapNotNull { it.manga?.toSManga(titleStyle) }
|
||||
|
||||
return MangasPage(mangaList, mangaList.size == limit)
|
||||
}
|
||||
|
@ -115,58 +121,26 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val showAdult = preferences.getBoolean(SHOW_ADULT_PREF, false)
|
||||
var country = "ALL"
|
||||
val includeGenres = mutableListOf<String>()
|
||||
val excludeGenres = mutableListOf<String>()
|
||||
val payloadObj = ApiSearchPayload(
|
||||
query = query,
|
||||
size = limit,
|
||||
page = page,
|
||||
genres = filters.firstInstanceOrNull<GenreFilter>()?.included,
|
||||
excludeGenres = filters.firstInstanceOrNull<GenreFilter>()?.excluded,
|
||||
translationType = "sub",
|
||||
countryOrigin = filters.firstInstanceOrNull<CountryFilter>()?.getValue() ?: "ALL",
|
||||
allowAdult = preferences.allowAdult,
|
||||
)
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> {
|
||||
filter.state.forEach { genreState ->
|
||||
when (genreState.state) {
|
||||
STATE_INCLUDE -> includeGenres.add(genreState.name)
|
||||
STATE_EXCLUDE -> excludeGenres.add(genreState.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CountryFilter -> {
|
||||
country = filter.getValue()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
putJsonObject("search") {
|
||||
if (includeGenres.isNotEmpty() || excludeGenres.isNotEmpty()) {
|
||||
put("genres", JsonArray(includeGenres.map { JsonPrimitive(it) }))
|
||||
put("excludeGenres", JsonArray(excludeGenres.map { JsonPrimitive(it) }))
|
||||
}
|
||||
if (query.isNotEmpty()) put("query", query)
|
||||
put("allowAdult", showAdult)
|
||||
put("allowUnknown", false)
|
||||
put("isManga", true)
|
||||
}
|
||||
put("limit", limit)
|
||||
put("page", page)
|
||||
put("translationType", "sub")
|
||||
put("countryOrigin", country)
|
||||
}
|
||||
put("query", SEARCH_QUERY)
|
||||
}
|
||||
|
||||
return apiRequest(payload)
|
||||
return apiRequest(payloadObj)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val result = json.decodeFromString<ApiSearchResponse>(response.body.string())
|
||||
val result = response.parseAs<ApiSearchResponse>()
|
||||
val titleStyle = preferences.titlePref
|
||||
|
||||
val mangaList = result.data.mangas.edges
|
||||
.map { manga ->
|
||||
toSManga(manga)
|
||||
}
|
||||
val mangaList = result.data.mangas.mangas
|
||||
.map { it.toSManga(titleStyle) }
|
||||
|
||||
return MangasPage(mangaList, mangaList.size == limit)
|
||||
}
|
||||
|
@ -176,22 +150,15 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
/* Details */
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val mangaId = manga.url.split("/")[2]
|
||||
val payloadObj = ApiIDPayload(mangaId, DETAILS_QUERY)
|
||||
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("_id", mangaId)
|
||||
}
|
||||
put("query", DETAILS_QUERY)
|
||||
}
|
||||
|
||||
return apiRequest(payload)
|
||||
return apiRequest(payloadObj)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val result = json.decodeFromString<ApiMangaDetailsResponse>(response.body.string())
|
||||
val manga = result.data.manga
|
||||
val result = response.parseAs<ApiMangaDetailsResponse>()
|
||||
|
||||
return toSManga(manga)
|
||||
return result.data.manga.toSManga(preferences.titlePref)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
|
@ -209,30 +176,44 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val mangaId = manga.url.split("/")[2]
|
||||
val payloadObj = ApiIDPayload(mangaId, CHAPTERS_QUERY)
|
||||
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("_id", mangaId)
|
||||
}
|
||||
put("query", CHAPTERS_QUERY)
|
||||
}
|
||||
return apiRequest(payloadObj)
|
||||
}
|
||||
|
||||
return apiRequest(payload)
|
||||
private fun chapterDetailsRequest(manga: SManga, start: String, end: String): Request {
|
||||
val mangaId = manga.url.split("/")[2]
|
||||
val payloadObj = ApiChapterListDetailsPayload(mangaId, start.toFloat(), end.toFloat())
|
||||
|
||||
return apiRequest(payloadObj)
|
||||
}
|
||||
|
||||
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||
val result = json.decodeFromString<ApiChapterListResponse>(response.body.string())
|
||||
val result = response.parseAs<ApiChapterListResponse>()
|
||||
val chapters = result.data.manga.chapters.sub
|
||||
?.sortedBy { it.toFloat() }
|
||||
?: return emptyList()
|
||||
|
||||
val chapters = result.data.manga.availableChaptersDetail.sub
|
||||
val chapterDetails = client.newCall(
|
||||
chapterDetailsRequest(manga, chapters.first(), chapters.last()),
|
||||
).execute()
|
||||
.use {
|
||||
it.parseAs<ApiChapterListDetailsResponse>()
|
||||
}.data.chapterList
|
||||
?.sortedBy { it.chapterNum }
|
||||
|
||||
val mangaUrl = manga.url.substringAfter("/manga/")
|
||||
|
||||
return chapters?.map { chapter ->
|
||||
return chapterDetails?.zip(chapters)?.map { (details, chapterNum) ->
|
||||
SChapter.create().apply {
|
||||
name = "Chapter $chapter"
|
||||
url = "/read/$mangaUrl/chapter-$chapter-sub"
|
||||
name = "Chapter $chapterNum"
|
||||
if (!details.title.isNullOrEmpty() && !details.title.contains(numberRegex)) {
|
||||
name += ": ${details.title}"
|
||||
}
|
||||
url = "/read/$mangaUrl/chapter-$chapterNum-sub"
|
||||
date_upload = details.uploadDates?.sub.parseDate()
|
||||
}
|
||||
} ?: emptyList()
|
||||
}?.reversed() ?: emptyList()
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
|
@ -257,24 +238,21 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
val mangaId = chapterUrl[2]
|
||||
val chapterNo = chapterUrl[4].split("-")[1]
|
||||
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("mangaId", mangaId)
|
||||
put("translationType", "sub")
|
||||
put("chapterString", chapterNo)
|
||||
}
|
||||
put("query", PAGE_QUERY)
|
||||
}
|
||||
val payloadObj = ApiPageListPayload(
|
||||
id = mangaId,
|
||||
chapterNum = chapterNo,
|
||||
translationType = "sub",
|
||||
)
|
||||
|
||||
return apiRequest(payload)
|
||||
return apiRequest(payloadObj)
|
||||
}
|
||||
|
||||
private fun pageListParse(response: Response, chapter: SChapter): List<Page> {
|
||||
val result = json.decodeFromString<ApiPageListResponse>(response.body.string())
|
||||
val pages = result.data.chapterPages?.edges?.get(0) ?: return emptyList()
|
||||
val pages = result.data.pageList?.serverList?.get(0) ?: return emptyList()
|
||||
|
||||
val imageDomain = if (!pages.pictureUrlHead.isNullOrEmpty()) {
|
||||
pages.pictureUrlHead.let { server ->
|
||||
val imageDomain = if (!pages.serverUrl.isNullOrEmpty()) {
|
||||
pages.serverUrl.let { server ->
|
||||
if (server.matches(urlRegex)) {
|
||||
server
|
||||
} else {
|
||||
|
@ -301,7 +279,7 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
return pages.pictureUrls.mapIndexed { index, image ->
|
||||
Page(
|
||||
index = index,
|
||||
imageUrl = "$imageDomain${image.url}",
|
||||
imageUrl = "$imageDomain${image.url}#page",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -315,106 +293,99 @@ class AllAnime : ConfigurableSource, HttpSource() {
|
|||
}
|
||||
|
||||
/* Helpers */
|
||||
private fun apiRequest(payload: JsonObject): Request {
|
||||
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||
private inline fun <reified T> apiRequest(payloadObj: T): Request {
|
||||
val payload = json.encodeToString(payloadObj)
|
||||
.toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.add("Content-Length", body.contentLength().toString())
|
||||
.add("Content-Type", body.contentType().toString())
|
||||
.add("Content-Length", payload.contentLength().toString())
|
||||
.add("Content-Type", payload.contentType().toString())
|
||||
.build()
|
||||
|
||||
return POST(apiUrl, newHeaders, body)
|
||||
return POST(apiUrl, newHeaders, payload)
|
||||
}
|
||||
|
||||
private fun toSManga(manga: Manga): SManga {
|
||||
val titleStyle = preferences.getString(TITLE_PREF, "romaji")!!
|
||||
private inline fun <reified T> Response.parseAs(): T = json.decodeFromString(body.string())
|
||||
|
||||
return SManga.create().apply {
|
||||
title = when (titleStyle) {
|
||||
"romaji" -> manga.name
|
||||
"eng" -> manga.englishName ?: manga.name
|
||||
else -> manga.nativeName ?: manga.name
|
||||
}
|
||||
url = "/manga/${manga._id}/${manga.name.titleToSlug()}"
|
||||
thumbnail_url = manga.thumbnail.parseThumbnailUrl()
|
||||
description = Jsoup.parse(
|
||||
manga.description?.replace("<br>", "br2n") ?: "",
|
||||
).text().replace("br2n", "\n")
|
||||
description += if (manga.altNames != null) {
|
||||
"\n\nAlternative Names: ${manga.altNames.joinToString { it.trim() }}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
if (manga.authors?.isNotEmpty() == true) {
|
||||
author = manga.authors.first().trim()
|
||||
artist = author
|
||||
}
|
||||
genre = "${manga.genres?.joinToString { it.trim() }}, ${manga.tags?.joinToString { it.trim() }}"
|
||||
status = manga.status.parseStatus()
|
||||
}
|
||||
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
||||
filterIsInstance<R>().firstOrNull()
|
||||
|
||||
private fun String?.parseDate(): Long {
|
||||
return runCatching {
|
||||
dateFormat.parse(this!!)!!.time
|
||||
}.getOrDefault(0L)
|
||||
}
|
||||
|
||||
private fun String.parseThumbnailUrl(): String {
|
||||
return if (this.matches(urlRegex)) {
|
||||
this
|
||||
} else {
|
||||
"$image_cdn$this?w=250"
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.parseStatus(): Int {
|
||||
if (this == null) {
|
||||
return SManga.UNKNOWN
|
||||
}
|
||||
|
||||
return when {
|
||||
this.contains("releasing", true) -> SManga.ONGOING
|
||||
this.contains("finished", true) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.titleToSlug() = this.trim()
|
||||
.lowercase(Locale.US)
|
||||
.replace(titleSpecialCharactersRegex, "-")
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = DOMAIN_PREF
|
||||
title = "Preferred domain"
|
||||
entries = arrayOf("allanime.to", "allanime.co")
|
||||
entryValues = arrayOf("allanime.to", "allanime.co")
|
||||
setDefaultValue("allanime.to")
|
||||
setDefaultValue(DOMAIN_PREF_DEFAULT)
|
||||
summary = "Requires App Restart"
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = IMAGE_QUALITY_PREF
|
||||
title = "Image Quality"
|
||||
entries = arrayOf("Original", "Wp-800", "Wp-480")
|
||||
entryValues = arrayOf("original", "800", "480")
|
||||
setDefaultValue(IMAGE_QUALITY_PREF_DEFAULT)
|
||||
summary = "Warning: Wp quality servers can be slow and might not work sometimes"
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = TITLE_PREF
|
||||
title = "Preferred Title Style"
|
||||
entries = arrayOf("Romaji", "English", "Native")
|
||||
entryValues = arrayOf("romaji", "eng", "native")
|
||||
setDefaultValue("romaji")
|
||||
setDefaultValue(TITLE_PREF_DEFAULT)
|
||||
summary = "%s"
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = SHOW_ADULT_PREF
|
||||
title = "Show Adult Content"
|
||||
setDefaultValue(false)
|
||||
setDefaultValue(SHOW_ADULT_PREF_DEFAULT)
|
||||
}.let { screen.addPreference(it) }
|
||||
}
|
||||
|
||||
private val SharedPreferences.domainPref
|
||||
get() = getString(DOMAIN_PREF, DOMAIN_PREF_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.titlePref
|
||||
get() = getString(TITLE_PREF, TITLE_PREF_DEFAULT)
|
||||
|
||||
private val SharedPreferences.allowAdult
|
||||
get() = getBoolean(SHOW_ADULT_PREF, SHOW_ADULT_PREF_DEFAULT)
|
||||
|
||||
private val SharedPreferences.imageQuality
|
||||
get() = getString(IMAGE_QUALITY_PREF, IMAGE_QUALITY_PREF_DEFAULT)!!
|
||||
|
||||
companion object {
|
||||
private const val limit = 26
|
||||
private const val limit = 20
|
||||
private val numberRegex by lazy { Regex("\\d") }
|
||||
val whitespace by lazy { Regex("\\s+") }
|
||||
val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||
}
|
||||
const val SEARCH_PREFIX = "id:"
|
||||
private const val image_cdn = "https://wp.youtube-anime.com/aln.youtube-anime.com/"
|
||||
const val thumbnail_cdn = "https://wp.youtube-anime.com/aln.youtube-anime.com/"
|
||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private val urlRegex = Regex("^https?://.*")
|
||||
private val titleSpecialCharactersRegex = Regex("[^a-z\\d]+")
|
||||
val urlRegex = Regex("^https?://.*")
|
||||
private const val image_cdn = "https://wp.youtube-anime.com"
|
||||
private val imageQualityRegex = Regex("^https?://(.*)#.*")
|
||||
val titleSpecialCharactersRegex = Regex("[^a-z\\d]+")
|
||||
private val imageUrlFromPageRegex = Regex("selectedPicturesServer:\\[\\{.*?url:\"(.*?)\".*?\\}\\]")
|
||||
|
||||
private const val DOMAIN_PREF = "pref_domain"
|
||||
private const val DOMAIN_PREF_DEFAULT = "allanime.to"
|
||||
private const val TITLE_PREF = "pref_title"
|
||||
private const val TITLE_PREF_DEFAULT = "romaji"
|
||||
private const val SHOW_ADULT_PREF = "pref_adult"
|
||||
private const val SHOW_ADULT_PREF_DEFAULT = false
|
||||
private const val IMAGE_QUALITY_PREF = "pref_quality"
|
||||
private const val IMAGE_QUALITY_PREF_DEFAULT = "original"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
package eu.kanade.tachiyomi.extension.en.allanime
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class ApiPopularResponse(
|
||||
val data: PopularResultData,
|
||||
val data: PopularResponseData,
|
||||
) {
|
||||
@Serializable
|
||||
data class PopularResultData(
|
||||
val queryPopular: QueryPopularData,
|
||||
data class PopularResponseData(
|
||||
@SerialName("queryPopular") val popular: PopularData,
|
||||
) {
|
||||
@Serializable
|
||||
data class QueryPopularData(
|
||||
val recommendations: List<Recommendation>,
|
||||
data class PopularData(
|
||||
@SerialName("recommendations") val mangas: List<Popular>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Recommendation(
|
||||
val anyCard: Manga? = null,
|
||||
data class Popular(
|
||||
@SerialName("anyCard") val manga: SearchManga? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -24,19 +28,34 @@ data class ApiPopularResponse(
|
|||
|
||||
@Serializable
|
||||
data class ApiSearchResponse(
|
||||
val data: SearchResultData,
|
||||
val data: SearchResponseData,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultData(
|
||||
data class SearchResponseData(
|
||||
val mangas: SearchResultMangas,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultMangas(
|
||||
val edges: List<Manga>,
|
||||
@SerialName("edges") val mangas: List<SearchManga>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SearchManga(
|
||||
@SerialName("_id") val id: String,
|
||||
val name: String,
|
||||
val thumbnail: String? = null,
|
||||
val englishName: String? = null,
|
||||
val nativeName: String? = null,
|
||||
) {
|
||||
fun toSManga(titleStyle: String?) = SManga.create().apply {
|
||||
title = titleStyle.preferedName(name, englishName, nativeName)
|
||||
url = "/manga/$id/${name.titleToSlug()}"
|
||||
thumbnail_url = thumbnail?.parseThumbnailUrl()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiMangaDetailsResponse(
|
||||
val data: MangaDetailsData,
|
||||
|
@ -44,24 +63,43 @@ data class ApiMangaDetailsResponse(
|
|||
@Serializable
|
||||
data class MangaDetailsData(
|
||||
val manga: Manga,
|
||||
)
|
||||
) {
|
||||
@Serializable
|
||||
data class Manga(
|
||||
@SerialName("_id") val id: String,
|
||||
val name: String,
|
||||
val thumbnail: String? = null,
|
||||
val description: String? = null,
|
||||
val authors: List<String>? = emptyList(),
|
||||
val genres: List<String>? = emptyList(),
|
||||
val tags: List<String>? = emptyList(),
|
||||
val status: String? = null,
|
||||
val altNames: List<String>? = emptyList(),
|
||||
val englishName: String? = null,
|
||||
val nativeName: String? = null,
|
||||
) {
|
||||
fun toSManga(titleStyle: String?) = SManga.create().apply {
|
||||
title = titleStyle.preferedName(name, englishName, nativeName)
|
||||
url = "/manga/$id/${name.titleToSlug()}"
|
||||
thumbnail_url = thumbnail?.parseThumbnailUrl()
|
||||
description = this@Manga.description?.parseDescription()
|
||||
if (!altNames.isNullOrEmpty()) {
|
||||
description += altNames.joinToString(
|
||||
prefix = "\n\nAlternative Names:\n* ",
|
||||
separator = "\n* ",
|
||||
) { it.trim() }
|
||||
}
|
||||
if (authors?.isNotEmpty() == true) {
|
||||
author = authors.first().trim()
|
||||
artist = author
|
||||
}
|
||||
genre = "${genres?.joinToString { it.trim() }}, ${tags?.joinToString { it.trim() }}"
|
||||
status = this@Manga.status.parseStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Manga(
|
||||
val _id: String,
|
||||
val name: String,
|
||||
val thumbnail: String,
|
||||
val description: String?,
|
||||
val authors: List<String>?,
|
||||
val genres: List<String>?,
|
||||
val tags: List<String>?,
|
||||
val status: String?,
|
||||
val altNames: List<String>?,
|
||||
val englishName: String? = null,
|
||||
val nativeName: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiChapterListResponse(
|
||||
val data: ChapterListData,
|
||||
|
@ -72,7 +110,7 @@ data class ApiChapterListResponse(
|
|||
) {
|
||||
@Serializable
|
||||
data class ChapterList(
|
||||
val availableChaptersDetail: AvailableChapters,
|
||||
@SerialName("availableChaptersDetail") val chapters: AvailableChapters,
|
||||
) {
|
||||
@Serializable
|
||||
data class AvailableChapters(
|
||||
|
@ -82,21 +120,43 @@ data class ApiChapterListResponse(
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiChapterListDetailsResponse(
|
||||
val data: ChapterListData,
|
||||
) {
|
||||
@Serializable
|
||||
data class ChapterListData(
|
||||
@SerialName("episodeInfos") val chapterList: List<ChapterData>? = emptyList(),
|
||||
) {
|
||||
@Serializable
|
||||
data class ChapterData(
|
||||
@SerialName("episodeIdNum") val chapterNum: Float,
|
||||
@SerialName("notes") val title: String? = null,
|
||||
val uploadDates: DateDto? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class DateDto(
|
||||
val sub: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiPageListResponse(
|
||||
val data: PageListData,
|
||||
) {
|
||||
@Serializable
|
||||
data class PageListData(
|
||||
val chapterPages: PageList?,
|
||||
@SerialName("chapterPages") val pageList: PageList?,
|
||||
) {
|
||||
@Serializable
|
||||
data class PageList(
|
||||
val edges: List<Servers>?,
|
||||
@SerialName("edges") val serverList: List<Servers>?,
|
||||
) {
|
||||
@Serializable
|
||||
data class Servers(
|
||||
val pictureUrlHead: String? = null,
|
||||
@SerialName("pictureUrlHead") val serverUrl: String? = null,
|
||||
val pictureUrls: List<PageUrl>,
|
||||
) {
|
||||
@Serializable
|
||||
|
@ -107,3 +167,41 @@ data class ApiPageListResponse(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.parseThumbnailUrl(): String {
|
||||
return if (this.matches(AllAnime.urlRegex)) {
|
||||
this
|
||||
} else {
|
||||
"${AllAnime.thumbnail_cdn}$this?w=250"
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.parseStatus(): Int {
|
||||
if (this == null) {
|
||||
return SManga.UNKNOWN
|
||||
}
|
||||
|
||||
return when {
|
||||
this.contains("releasing", true) -> SManga.ONGOING
|
||||
this.contains("finished", true) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.titleToSlug() = this.trim()
|
||||
.lowercase(Locale.US)
|
||||
.replace(AllAnime.titleSpecialCharactersRegex, "-")
|
||||
|
||||
private fun String?.preferedName(name: String, englishName: String?, nativeName: String?): String {
|
||||
return when (this) {
|
||||
"eng" -> englishName
|
||||
"native" -> nativeName
|
||||
else -> name
|
||||
} ?: name
|
||||
}
|
||||
|
||||
private fun String.parseDescription(): String {
|
||||
return Jsoup.parse(
|
||||
this.replace("<br>", "br2n"),
|
||||
).text().replace("br2n", "\n")
|
||||
}
|
||||
|
|
|
@ -11,7 +11,13 @@ internal class CountryFilter(name: String, private val countries: List<Pair<Stri
|
|||
}
|
||||
|
||||
internal class GenreFilter(title: String, genres: List<Genre>) :
|
||||
Filter.Group<Genre>(title, genres)
|
||||
Filter.Group<Genre>(title, genres) {
|
||||
val included: List<String>
|
||||
get() = state.filter { it.isIncluded() }.map { it.name }
|
||||
|
||||
val excluded: List<String>
|
||||
get() = state.filter { it.isExcluded() }.map { it.name }
|
||||
}
|
||||
|
||||
private val genreList: List<Genre> = listOf(
|
||||
Genre("4 Koma"),
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
package eu.kanade.tachiyomi.extension.en.allanime
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ApiPopularPayload(
|
||||
val variables: ApiPopularVariables,
|
||||
val query: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class ApiPopularVariables(
|
||||
val type: String,
|
||||
val size: Int,
|
||||
val dateRange: Int,
|
||||
val page: Int,
|
||||
val allowAdult: Boolean,
|
||||
val allowUnknown: Boolean,
|
||||
)
|
||||
|
||||
constructor(
|
||||
type: String = "manga",
|
||||
size: Int,
|
||||
dateRange: Int,
|
||||
page: Int,
|
||||
allowAdult: Boolean = false,
|
||||
allowUnknown: Boolean = false,
|
||||
) : this(
|
||||
ApiPopularVariables(
|
||||
type = type,
|
||||
size = size,
|
||||
dateRange = dateRange,
|
||||
page = page,
|
||||
allowAdult = allowAdult,
|
||||
allowUnknown = allowUnknown,
|
||||
),
|
||||
POPULAR_QUERY,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiSearchPayload(
|
||||
val variables: ApiSearchVariables,
|
||||
val query: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class ApiSearchVariables(
|
||||
val search: SearchPayload,
|
||||
val limit: Int,
|
||||
val page: Int,
|
||||
val translationType: String,
|
||||
val countryOrigin: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchPayload(
|
||||
val query: String,
|
||||
val genres: List<String>?,
|
||||
val excludeGenres: List<String>?,
|
||||
val isManga: Boolean,
|
||||
val allowAdult: Boolean,
|
||||
val allowUnknown: Boolean,
|
||||
)
|
||||
|
||||
constructor(
|
||||
query: String,
|
||||
size: Int,
|
||||
page: Int,
|
||||
genres: List<String>?,
|
||||
excludeGenres: List<String>?,
|
||||
translationType: String,
|
||||
countryOrigin: String,
|
||||
isManga: Boolean = true,
|
||||
allowAdult: Boolean = false,
|
||||
allowUnknown: Boolean = false,
|
||||
) : this(
|
||||
ApiSearchVariables(
|
||||
search = SearchPayload(
|
||||
query = query,
|
||||
genres = genres,
|
||||
excludeGenres = excludeGenres,
|
||||
isManga = isManga,
|
||||
allowAdult = allowAdult,
|
||||
allowUnknown = allowUnknown,
|
||||
),
|
||||
limit = size,
|
||||
page = page,
|
||||
translationType = translationType,
|
||||
countryOrigin = countryOrigin,
|
||||
),
|
||||
SEARCH_QUERY,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiIDPayload(
|
||||
val variables: ApiIDVariables,
|
||||
val query: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class ApiIDVariables(
|
||||
val id: String,
|
||||
)
|
||||
|
||||
constructor(
|
||||
id: String,
|
||||
graphqlQuery: String,
|
||||
) : this(
|
||||
ApiIDVariables(id),
|
||||
graphqlQuery,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiChapterListDetailsPayload(
|
||||
val variables: ApiChapterDetailsVariables,
|
||||
val query: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class ApiChapterDetailsVariables(
|
||||
val id: String,
|
||||
val chapterNumStart: Float,
|
||||
val chapterNumEnd: Float,
|
||||
)
|
||||
|
||||
constructor(
|
||||
id: String,
|
||||
chapterNumStart: Float,
|
||||
chapterNumEnd: Float,
|
||||
) : this(
|
||||
ApiChapterDetailsVariables(
|
||||
id = "manga@$id",
|
||||
chapterNumStart = chapterNumStart,
|
||||
chapterNumEnd = chapterNumEnd,
|
||||
),
|
||||
CHAPTERS_DETAILS_QUERY,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiPageListPayload(
|
||||
val variables: ApiPageListVariables,
|
||||
val query: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class ApiPageListVariables(
|
||||
val id: String,
|
||||
val chapterNum: String,
|
||||
val translationType: String,
|
||||
)
|
||||
|
||||
constructor(
|
||||
id: String,
|
||||
chapterNum: String,
|
||||
translationType: String,
|
||||
) : this(
|
||||
ApiPageListVariables(
|
||||
id = id,
|
||||
chapterNum = chapterNum,
|
||||
translationType = translationType,
|
||||
),
|
||||
PAGE_QUERY,
|
||||
)
|
||||
}
|
|
@ -1,21 +1,24 @@
|
|||
package eu.kanade.tachiyomi.extension.en.allanime
|
||||
|
||||
fun buildQuery(queryAction: () -> String): String {
|
||||
import eu.kanade.tachiyomi.extension.en.allanime.AllAnime.Companion.whitespace
|
||||
|
||||
private fun buildQuery(queryAction: () -> String): String {
|
||||
return queryAction()
|
||||
.trimIndent()
|
||||
.replace(whitespace, " ")
|
||||
.replace("%", "$")
|
||||
}
|
||||
|
||||
val POPULAR_QUERY: String = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%type: VaildPopularTypeEnumType!
|
||||
%size: Int!
|
||||
%page: Int
|
||||
%dateRange: Int
|
||||
%allowAdult: Boolean
|
||||
%allowUnknown: Boolean
|
||||
) {
|
||||
query (
|
||||
%type: VaildPopularTypeEnumType!
|
||||
%size: Int!
|
||||
%page: Int
|
||||
%dateRange: Int
|
||||
%allowAdult: Boolean
|
||||
%allowUnknown: Boolean
|
||||
) {
|
||||
queryPopular(
|
||||
type: %type
|
||||
size: %size
|
||||
|
@ -40,13 +43,13 @@ val POPULAR_QUERY: String = buildQuery {
|
|||
|
||||
val SEARCH_QUERY: String = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%search: SearchInput
|
||||
%limit: Int
|
||||
%page: Int
|
||||
%translationType: VaildTranslationTypeMangaEnumType
|
||||
%countryOrigin: VaildCountryOriginEnumType
|
||||
) {
|
||||
query (
|
||||
%search: SearchInput
|
||||
%limit: Int
|
||||
%page: Int
|
||||
%translationType: VaildTranslationTypeMangaEnumType
|
||||
%countryOrigin: VaildCountryOriginEnumType
|
||||
) {
|
||||
mangas(
|
||||
search: %search
|
||||
limit: %limit
|
||||
|
@ -68,10 +71,8 @@ val SEARCH_QUERY: String = buildQuery {
|
|||
|
||||
val DETAILS_QUERY: String = buildQuery {
|
||||
"""
|
||||
query (%_id: String!) {
|
||||
manga(
|
||||
_id: %_id
|
||||
) {
|
||||
query (%id: String!) {
|
||||
manga(_id: %id) {
|
||||
_id
|
||||
name
|
||||
thumbnail
|
||||
|
@ -90,27 +91,41 @@ val DETAILS_QUERY: String = buildQuery {
|
|||
|
||||
val CHAPTERS_QUERY: String = buildQuery {
|
||||
"""
|
||||
query (%_id: String!) {
|
||||
manga(
|
||||
_id: %_id
|
||||
) {
|
||||
query (%id: String!) {
|
||||
manga(_id: %id) {
|
||||
availableChaptersDetail
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val CHAPTERS_DETAILS_QUERY: String = buildQuery {
|
||||
"""
|
||||
query (%id: String!, %chapterNumStart: Float!, %chapterNumEnd: Float!) {
|
||||
episodeInfos(
|
||||
showId: %id
|
||||
episodeNumStart: %chapterNumStart
|
||||
episodeNumEnd: %chapterNumEnd
|
||||
) {
|
||||
episodeIdNum
|
||||
notes
|
||||
uploadDates
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val PAGE_QUERY: String = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%mangaId: String!,
|
||||
%translationType: VaildTranslationTypeMangaEnumType!,
|
||||
%chapterString: String!
|
||||
) {
|
||||
query (
|
||||
%id: String!
|
||||
%translationType: VaildTranslationTypeMangaEnumType!
|
||||
%chapterNum: String!
|
||||
) {
|
||||
chapterPages(
|
||||
mangaId: %mangaId
|
||||
mangaId: %id
|
||||
translationType: %translationType
|
||||
chapterString: %chapterString
|
||||
chapterString: %chapterNum
|
||||
) {
|
||||
edges {
|
||||
pictureUrls
|
||||
|
|
Loading…
Reference in New Issue