AllAnime refactor (#16660)

* AllAnime refactor

* code cleanup
* chapter titles\dates
* image quality setting

* [skip ci] add more warning
This commit is contained in:
AwkwardPeak7 2023-06-06 14:19:32 +05:00 committed by GitHub
parent 1b47daff10
commit 6aa5ac0f17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 493 additions and 240 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'AllAnime' extName = 'AllAnime'
pkgNameSuffix = 'en.allanime' pkgNameSuffix = 'en.allanime'
extClass = '.AllAnime' extClass = '.AllAnime'
extVersionCode = 2 extVersionCode = 3
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -10,8 +10,6 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.ConfigurableSource 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page 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.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json 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.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class AllAnime : ConfigurableSource, HttpSource() { class AllAnime : ConfigurableSource, HttpSource() {
@ -48,18 +40,41 @@ class AllAnime : ConfigurableSource, HttpSource() {
override val supportsLatest = true 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 = private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) 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" override val baseUrl = "https://$domain"
private val apiUrl = "https://api.$domain/allanimeapi" private val apiUrl = "https://api.$domain/allanimeapi"
override val client: OkHttpClient = network.cloudflareClient.newBuilder() 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) .rateLimitHost(apiUrl.toHttpUrl(), 1)
.build() .build()
@ -68,31 +83,22 @@ class AllAnime : ConfigurableSource, HttpSource() {
/* Popular */ /* Popular */
override fun popularMangaRequest(page: Int): Request { 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 { return apiRequest(payloadObj)
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)
} }
override fun popularMangaParse(response: Response): MangasPage { 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 val mangaList = result.data.popular.mangas
.mapNotNull { it.anyCard } .mapNotNull { it.manga?.toSManga(titleStyle) }
.map { manga ->
toSManga(manga)
}
return MangasPage(mangaList, mangaList.size == limit) return MangasPage(mangaList, mangaList.size == limit)
} }
@ -115,58 +121,26 @@ class AllAnime : ConfigurableSource, HttpSource() {
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val showAdult = preferences.getBoolean(SHOW_ADULT_PREF, false) val payloadObj = ApiSearchPayload(
var country = "ALL" query = query,
val includeGenres = mutableListOf<String>() size = limit,
val excludeGenres = mutableListOf<String>() 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 -> return apiRequest(payloadObj)
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)
} }
override fun searchMangaParse(response: Response): MangasPage { 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 val mangaList = result.data.mangas.mangas
.map { manga -> .map { it.toSManga(titleStyle) }
toSManga(manga)
}
return MangasPage(mangaList, mangaList.size == limit) return MangasPage(mangaList, mangaList.size == limit)
} }
@ -176,22 +150,15 @@ class AllAnime : ConfigurableSource, HttpSource() {
/* Details */ /* Details */
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
val mangaId = manga.url.split("/")[2] val mangaId = manga.url.split("/")[2]
val payloadObj = ApiIDPayload(mangaId, DETAILS_QUERY)
val payload = buildJsonObject { return apiRequest(payloadObj)
putJsonObject("variables") {
put("_id", mangaId)
}
put("query", DETAILS_QUERY)
}
return apiRequest(payload)
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val result = json.decodeFromString<ApiMangaDetailsResponse>(response.body.string()) val result = response.parseAs<ApiMangaDetailsResponse>()
val manga = result.data.manga
return toSManga(manga) return result.data.manga.toSManga(preferences.titlePref)
} }
override fun getMangaUrl(manga: SManga): String { override fun getMangaUrl(manga: SManga): String {
@ -209,30 +176,44 @@ class AllAnime : ConfigurableSource, HttpSource() {
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
val mangaId = manga.url.split("/")[2] val mangaId = manga.url.split("/")[2]
val payloadObj = ApiIDPayload(mangaId, CHAPTERS_QUERY)
val payload = buildJsonObject { return apiRequest(payloadObj)
putJsonObject("variables") {
put("_id", mangaId)
}
put("query", CHAPTERS_QUERY)
} }
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> { 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/") val mangaUrl = manga.url.substringAfter("/manga/")
return chapters?.map { chapter -> return chapterDetails?.zip(chapters)?.map { (details, chapterNum) ->
SChapter.create().apply { SChapter.create().apply {
name = "Chapter $chapter" name = "Chapter $chapterNum"
url = "/read/$mangaUrl/chapter-$chapter-sub" if (!details.title.isNullOrEmpty() && !details.title.contains(numberRegex)) {
name += ": ${details.title}"
} }
} ?: emptyList() url = "/read/$mangaUrl/chapter-$chapterNum-sub"
date_upload = details.uploadDates?.sub.parseDate()
}
}?.reversed() ?: emptyList()
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
@ -257,24 +238,21 @@ class AllAnime : ConfigurableSource, HttpSource() {
val mangaId = chapterUrl[2] val mangaId = chapterUrl[2]
val chapterNo = chapterUrl[4].split("-")[1] val chapterNo = chapterUrl[4].split("-")[1]
val payload = buildJsonObject { val payloadObj = ApiPageListPayload(
putJsonObject("variables") { id = mangaId,
put("mangaId", mangaId) chapterNum = chapterNo,
put("translationType", "sub") translationType = "sub",
put("chapterString", chapterNo) )
}
put("query", PAGE_QUERY)
}
return apiRequest(payload) return apiRequest(payloadObj)
} }
private fun pageListParse(response: Response, chapter: SChapter): List<Page> { private fun pageListParse(response: Response, chapter: SChapter): List<Page> {
val result = json.decodeFromString<ApiPageListResponse>(response.body.string()) 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()) { val imageDomain = if (!pages.serverUrl.isNullOrEmpty()) {
pages.pictureUrlHead.let { server -> pages.serverUrl.let { server ->
if (server.matches(urlRegex)) { if (server.matches(urlRegex)) {
server server
} else { } else {
@ -301,7 +279,7 @@ class AllAnime : ConfigurableSource, HttpSource() {
return pages.pictureUrls.mapIndexed { index, image -> return pages.pictureUrls.mapIndexed { index, image ->
Page( Page(
index = index, index = index,
imageUrl = "$imageDomain${image.url}", imageUrl = "$imageDomain${image.url}#page",
) )
} }
} }
@ -315,68 +293,28 @@ class AllAnime : ConfigurableSource, HttpSource() {
} }
/* Helpers */ /* Helpers */
private fun apiRequest(payload: JsonObject): Request { private inline fun <reified T> apiRequest(payloadObj: T): Request {
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE) val payload = json.encodeToString(payloadObj)
.toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder() val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString()) .add("Content-Length", payload.contentLength().toString())
.add("Content-Type", body.contentType().toString()) .add("Content-Type", payload.contentType().toString())
.build() .build()
return POST(apiUrl, newHeaders, body) return POST(apiUrl, newHeaders, payload)
} }
private fun toSManga(manga: Manga): SManga { private inline fun <reified T> Response.parseAs(): T = json.decodeFromString(body.string())
val titleStyle = preferences.getString(TITLE_PREF, "romaji")!!
return SManga.create().apply { private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
title = when (titleStyle) { filterIsInstance<R>().firstOrNull()
"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 fun String.parseThumbnailUrl(): String { private fun String?.parseDate(): Long {
return if (this.matches(urlRegex)) { return runCatching {
this dateFormat.parse(this!!)!!.time
} else { }.getOrDefault(0L)
"$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) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
@ -384,37 +322,70 @@ class AllAnime : ConfigurableSource, HttpSource() {
title = "Preferred domain" title = "Preferred domain"
entries = arrayOf("allanime.to", "allanime.co") entries = arrayOf("allanime.to", "allanime.co")
entryValues = arrayOf("allanime.to", "allanime.co") entryValues = arrayOf("allanime.to", "allanime.co")
setDefaultValue("allanime.to") setDefaultValue(DOMAIN_PREF_DEFAULT)
summary = "Requires App Restart" summary = "Requires App Restart"
}.let { screen.addPreference(it) } }.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 { ListPreference(screen.context).apply {
key = TITLE_PREF key = TITLE_PREF
title = "Preferred Title Style" title = "Preferred Title Style"
entries = arrayOf("Romaji", "English", "Native") entries = arrayOf("Romaji", "English", "Native")
entryValues = arrayOf("romaji", "eng", "native") entryValues = arrayOf("romaji", "eng", "native")
setDefaultValue("romaji") setDefaultValue(TITLE_PREF_DEFAULT)
summary = "%s" summary = "%s"
}.let { screen.addPreference(it) } }.let { screen.addPreference(it) }
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ADULT_PREF key = SHOW_ADULT_PREF
title = "Show Adult Content" title = "Show Adult Content"
setDefaultValue(false) setDefaultValue(SHOW_ADULT_PREF_DEFAULT)
}.let { screen.addPreference(it) } }.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 { 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:" 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 JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private val urlRegex = Regex("^https?://.*") val urlRegex = Regex("^https?://.*")
private val titleSpecialCharactersRegex = Regex("[^a-z\\d]+") 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 val imageUrlFromPageRegex = Regex("selectedPicturesServer:\\[\\{.*?url:\"(.*?)\".*?\\}\\]")
private const val DOMAIN_PREF = "pref_domain" 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 = "pref_title"
private const val TITLE_PREF_DEFAULT = "romaji"
private const val SHOW_ADULT_PREF = "pref_adult" 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"
} }
} }

View File

@ -1,22 +1,26 @@
package eu.kanade.tachiyomi.extension.en.allanime package eu.kanade.tachiyomi.extension.en.allanime
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.util.Locale
@Serializable @Serializable
data class ApiPopularResponse( data class ApiPopularResponse(
val data: PopularResultData, val data: PopularResponseData,
) { ) {
@Serializable @Serializable
data class PopularResultData( data class PopularResponseData(
val queryPopular: QueryPopularData, @SerialName("queryPopular") val popular: PopularData,
) { ) {
@Serializable @Serializable
data class QueryPopularData( data class PopularData(
val recommendations: List<Recommendation>, @SerialName("recommendations") val mangas: List<Popular>,
) { ) {
@Serializable @Serializable
data class Recommendation( data class Popular(
val anyCard: Manga? = null, @SerialName("anyCard") val manga: SearchManga? = null,
) )
} }
} }
@ -24,19 +28,34 @@ data class ApiPopularResponse(
@Serializable @Serializable
data class ApiSearchResponse( data class ApiSearchResponse(
val data: SearchResultData, val data: SearchResponseData,
) { ) {
@Serializable @Serializable
data class SearchResultData( data class SearchResponseData(
val mangas: SearchResultMangas, val mangas: SearchResultMangas,
) { ) {
@Serializable @Serializable
data class SearchResultMangas( 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 @Serializable
data class ApiMangaDetailsResponse( data class ApiMangaDetailsResponse(
val data: MangaDetailsData, val data: MangaDetailsData,
@ -44,23 +63,42 @@ data class ApiMangaDetailsResponse(
@Serializable @Serializable
data class MangaDetailsData( data class MangaDetailsData(
val manga: Manga, val manga: Manga,
) ) {
}
@Serializable @Serializable
data class Manga( data class Manga(
val _id: String, @SerialName("_id") val id: String,
val name: String, val name: String,
val thumbnail: String, val thumbnail: String? = null,
val description: String?, val description: String? = null,
val authors: List<String>?, val authors: List<String>? = emptyList(),
val genres: List<String>?, val genres: List<String>? = emptyList(),
val tags: List<String>?, val tags: List<String>? = emptyList(),
val status: String?, val status: String? = null,
val altNames: List<String>?, val altNames: List<String>? = emptyList(),
val englishName: String? = null, val englishName: String? = null,
val nativeName: 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 @Serializable
data class ApiChapterListResponse( data class ApiChapterListResponse(
@ -72,7 +110,7 @@ data class ApiChapterListResponse(
) { ) {
@Serializable @Serializable
data class ChapterList( data class ChapterList(
val availableChaptersDetail: AvailableChapters, @SerialName("availableChaptersDetail") val chapters: AvailableChapters,
) { ) {
@Serializable @Serializable
data class AvailableChapters( 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 @Serializable
data class ApiPageListResponse( data class ApiPageListResponse(
val data: PageListData, val data: PageListData,
) { ) {
@Serializable @Serializable
data class PageListData( data class PageListData(
val chapterPages: PageList?, @SerialName("chapterPages") val pageList: PageList?,
) { ) {
@Serializable @Serializable
data class PageList( data class PageList(
val edges: List<Servers>?, @SerialName("edges") val serverList: List<Servers>?,
) { ) {
@Serializable @Serializable
data class Servers( data class Servers(
val pictureUrlHead: String? = null, @SerialName("pictureUrlHead") val serverUrl: String? = null,
val pictureUrls: List<PageUrl>, val pictureUrls: List<PageUrl>,
) { ) {
@Serializable @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")
}

View File

@ -11,7 +11,13 @@ internal class CountryFilter(name: String, private val countries: List<Pair<Stri
} }
internal class GenreFilter(title: String, genres: List<Genre>) : 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( private val genreList: List<Genre> = listOf(
Genre("4 Koma"), Genre("4 Koma"),

View File

@ -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,
)
}

View File

@ -1,8 +1,11 @@
package eu.kanade.tachiyomi.extension.en.allanime 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() return queryAction()
.trimIndent() .trimIndent()
.replace(whitespace, " ")
.replace("%", "$") .replace("%", "$")
} }
@ -68,10 +71,8 @@ val SEARCH_QUERY: String = buildQuery {
val DETAILS_QUERY: String = buildQuery { val DETAILS_QUERY: String = buildQuery {
""" """
query (%_id: String!) { query (%id: String!) {
manga( manga(_id: %id) {
_id: %_id
) {
_id _id
name name
thumbnail thumbnail
@ -90,27 +91,41 @@ val DETAILS_QUERY: String = buildQuery {
val CHAPTERS_QUERY: String = buildQuery { val CHAPTERS_QUERY: String = buildQuery {
""" """
query (%_id: String!) { query (%id: String!) {
manga( manga(_id: %id) {
_id: %_id
) {
availableChaptersDetail 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 { val PAGE_QUERY: String = buildQuery {
""" """
query ( query (
%mangaId: String!, %id: String!
%translationType: VaildTranslationTypeMangaEnumType!, %translationType: VaildTranslationTypeMangaEnumType!
%chapterString: String! %chapterNum: String!
) { ) {
chapterPages( chapterPages(
mangaId: %mangaId mangaId: %id
translationType: %translationType translationType: %translationType
chapterString: %chapterString chapterString: %chapterNum
) { ) {
edges { edges {
pictureUrls pictureUrls