AllAnime: fix filters not resetting and refactor data models (#17378)

* AllAnime: fix filters not resetting and refactor data models

* some changes

* correct domain in AndroidManifest for deep links
This commit is contained in:
AwkwardPeak7 2023-08-05 18:30:54 +05:00 committed by GitHub
parent 7b48737ba8
commit 92b8f4906f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 452 additions and 601 deletions

View File

@ -13,15 +13,10 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="allanime.to"/>
<data android:host="allanime.co"/>
<data
android:pathPattern="/manga/..*"
android:scheme="https" />
<data
android:pathPattern="/read/..*"
android:scheme="https" />
<data android:host="allanime.ai"/>
<data android:scheme="https"/>
<data android:pathPattern="/manga/..*"/>
<data android:pathPattern="/read/..*"/>
</intent-filter>
</activity>
</application>

View File

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

View File

@ -5,7 +5,10 @@ import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.buildApiHeaders
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.firstInstanceOrNull
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseAs
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.toJsonRequestBody
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -16,44 +19,30 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
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 okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import kotlinx.serialization.json.float
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class AllAnime : ConfigurableSource, HttpSource() {
override val name = "AllAnime"
override val lang = "en"
override val supportsLatest = true
private val json: Json = Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
coerceInputValues = true
}
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
override val baseUrl = "https://allanime.ai"
private val apiUrl = "https://api.allanime.day/api"
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
override val lang = "en"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client = network.cloudflareClient.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val frag = request.url.fragment
@ -80,24 +69,34 @@ class AllAnime : ConfigurableSource, HttpSource() {
/* Popular */
override fun popularMangaRequest(page: Int): Request {
val payloadObj = ApiPopularPayload(
val payload = GraphQL(
PopularVariables(
type = "manga",
size = limit,
dateRange = 0,
page = page,
allowAdult = preferences.allowAdult,
allowUnknown = false,
),
POPULAR_QUERY,
)
return apiRequest(payloadObj)
val requestBody = payload.toJsonRequestBody()
val apiHeaders = headersBuilder().buildApiHeaders(requestBody)
return POST(apiUrl, apiHeaders, requestBody)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = response.parseAs<ApiPopularResponse>()
val titleStyle = preferences.titlePref
val mangaList = result.data.popular.mangas
.mapNotNull { it.manga?.toSManga(titleStyle) }
.mapNotNull { it.manga?.toSManga() }
return MangasPage(mangaList, mangaList.size == limit)
val hasNextPage = result.data.popular.mangas.size == limit
return MangasPage(mangaList, hasNextPage)
}
/* Latest */
@ -118,28 +117,41 @@ class AllAnime : ConfigurableSource, HttpSource() {
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val payloadObj = ApiSearchPayload(
query = query,
size = limit,
page = page,
val payload = GraphQL(
SearchVariables(
search = SearchPayload(
query = query.takeUnless { it.isEmpty() },
sortBy = filters.firstInstanceOrNull<SortFilter>()?.getValue(),
genres = filters.firstInstanceOrNull<GenreFilter>()?.included,
excludeGenres = filters.firstInstanceOrNull<GenreFilter>()?.excluded,
isManga = true,
allowAdult = preferences.allowAdult,
allowUnknown = false,
),
size = limit,
page = page,
translationType = "sub",
countryOrigin = filters.firstInstanceOrNull<CountryFilter>()?.getValue() ?: "ALL",
allowAdult = preferences.allowAdult,
),
SEARCH_QUERY,
)
return apiRequest(payloadObj)
val requestBody = payload.toJsonRequestBody()
val apiHeaders = headersBuilder().buildApiHeaders(requestBody)
return POST(apiUrl, apiHeaders, requestBody)
}
override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<ApiSearchResponse>()
val titleStyle = preferences.titlePref
val mangaList = result.data.mangas.mangas
.map { it.toSManga(titleStyle) }
val mangaList = result.data.mangas.edges
.map(SearchManga::toSManga)
return MangasPage(mangaList, mangaList.size == limit)
val hasNextPage = result.data.mangas.edges.size == limit
return MangasPage(mangaList, hasNextPage)
}
override fun getFilterList() = getFilters()
@ -147,15 +159,23 @@ class AllAnime : ConfigurableSource, HttpSource() {
/* Details */
override fun mangaDetailsRequest(manga: SManga): Request {
val mangaId = manga.url.split("/")[2]
val payloadObj = ApiIDPayload(mangaId, DETAILS_QUERY)
return apiRequest(payloadObj)
val payload = GraphQL(
IDVariables(mangaId),
DETAILS_QUERY,
)
val requestBody = payload.toJsonRequestBody()
val apiHeaders = headersBuilder().buildApiHeaders(requestBody)
return POST(apiUrl, apiHeaders, requestBody)
}
override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<ApiMangaDetailsResponse>()
return result.data.manga.toSManga(preferences.titlePref)
return result.data.manga.toSManga()
}
override fun getMangaUrl(manga: SManga): String {
@ -173,44 +193,32 @@ class AllAnime : ConfigurableSource, HttpSource() {
override fun chapterListRequest(manga: SManga): Request {
val mangaId = manga.url.split("/")[2]
val payloadObj = ApiIDPayload(mangaId, CHAPTERS_QUERY)
return apiRequest(payloadObj)
}
val payload = GraphQL(
ChapterListVariables(
id = "manga@$mangaId",
chapterNumStart = 0f,
chapterNumEnd = 9999f,
),
CHAPTERS_QUERY,
)
private fun chapterDetailsRequest(manga: SManga, start: String, end: String): Request {
val mangaId = manga.url.split("/")[2]
val payloadObj = ApiChapterListDetailsPayload(mangaId, start.toFloat(), end.toFloat())
val requestBody = payload.toJsonRequestBody()
return apiRequest(payloadObj)
val apiHeaders = headersBuilder().buildApiHeaders(requestBody)
return POST(apiUrl, apiHeaders, requestBody)
}
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val result = response.parseAs<ApiChapterListResponse>()
val chapters = result.data.manga.chapters.sub
?.sortedBy { it.toFloat() }
?: return emptyList()
val chapterDetails = client.newCall(
chapterDetailsRequest(manga, chapters.first(), chapters.last()),
).execute()
.use {
it.parseAs<ApiChapterListDetailsResponse>()
}.data.chapterList
?.sortedBy { it.chapterNum }
val chapters = result.data.chapterList?.sortedByDescending { it.chapterNum.float }
?: return emptyList()
val mangaUrl = manga.url.substringAfter("/manga/")
return chapterDetails?.zip(chapters)?.map { (details, chapterNum) ->
SChapter.create().apply {
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()
}
}?.reversed() ?: emptyList()
return chapters.map { it.toSChapter(mangaUrl) }
}
override fun chapterListParse(response: Response): List<SChapter> {
@ -222,56 +230,38 @@ class AllAnime : ConfigurableSource, HttpSource() {
}
/* Pages */
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
pageListParse(response, chapter)
}
}
override fun pageListRequest(chapter: SChapter): Request {
val chapterUrl = chapter.url.split("/")
val mangaId = chapterUrl[2]
val chapterNo = chapterUrl[4].split("-")[1]
val payloadObj = ApiPageListPayload(
val payload = GraphQL(
PageListVariables(
id = mangaId,
chapterNum = chapterNo,
translationType = "sub",
),
PAGE_QUERY,
)
return apiRequest(payloadObj)
val requestBody = payload.toJsonRequestBody()
val apiHeaders = headersBuilder().buildApiHeaders(requestBody)
return POST(apiUrl, apiHeaders, requestBody)
}
private fun pageListParse(response: Response, chapter: SChapter): List<Page> {
val result = json.decodeFromString<ApiPageListResponse>(response.body.string())
val pages = result.data.pageList?.serverList?.get(0) ?: return emptyList()
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<ApiPageListResponse>()
val pages = result.data.pageList?.edges?.get(0) ?: return emptyList()
val imageDomain = if (!pages.serverUrl.isNullOrEmpty()) {
pages.serverUrl.let { server ->
val imageDomain = pages.serverUrl?.let { server ->
if (server.matches(urlRegex)) {
server
} else {
"https://$server"
}
}
} else {
// in rare cases, the api doesn't return server url
// for that, we try to parse the frontend html to get it
val chapterUrl = getChapterUrl(chapter)
val frontendRequest = GET(chapterUrl, headers)
val url = client.newCall(frontendRequest).execute().use { frontendResponse ->
val document = frontendResponse.asJsoup()
val script = document.select("script:containsData(window.__NUXT__)").firstOrNull()
imageUrlFromPageRegex.matchEntire(script.toString())
?.groupValues
?.getOrNull(1)
?.replace("\\u002F", "/")
?.substringBeforeLast(pages.pictureUrls?.first().toString(), "")
}
url?.takeIf { it.isNotEmpty() } ?: return emptyList()
}
} ?: return emptyList()
return pages.pictureUrls?.mapIndexed { index, image ->
Page(
@ -281,38 +271,10 @@ class AllAnime : ConfigurableSource, HttpSource() {
} ?: emptyList()
}
override fun pageListParse(response: Response): List<Page> {
throw UnsupportedOperationException("Not used")
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException("Not used")
}
/* Helpers */
private inline fun <reified T> apiRequest(payloadObj: T): Request {
val payload = json.encodeToString(payloadObj)
.toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", payload.contentLength().toString())
.add("Content-Type", payload.contentType().toString())
.build()
return POST(apiUrl, newHeaders, payload)
}
private inline fun <reified T> Response.parseAs(): T = json.decodeFromString(body.string())
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
filterIsInstance<R>().firstOrNull()
private fun String?.parseDate(): Long {
return runCatching {
dateFormat.parse(this!!)!!.time
}.getOrDefault(0L)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = IMAGE_QUALITY_PREF
@ -321,27 +283,15 @@ class AllAnime : ConfigurableSource, HttpSource() {
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(TITLE_PREF_DEFAULT)
summary = "%s"
}.let { screen.addPreference(it) }
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ADULT_PREF
title = "Show Adult Content"
setDefaultValue(SHOW_ADULT_PREF_DEFAULT)
}.let { screen.addPreference(it) }
}.also(screen::addPreference)
}
private val SharedPreferences.titlePref
get() = getString(TITLE_PREF, TITLE_PREF_DEFAULT)
private val SharedPreferences.allowAdult
get() = getBoolean(SHOW_ADULT_PREF, SHOW_ADULT_PREF_DEFAULT)
@ -350,22 +300,11 @@ class AllAnime : ConfigurableSource, HttpSource() {
companion object {
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 thumbnail_cdn = "https://wp.youtube-anime.com/aln.youtube-anime.com/"
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
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 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"

View File

@ -1,45 +1,53 @@
package eu.kanade.tachiyomi.extension.en.allanime
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseDate
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseDescription
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseStatus
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseThumbnailUrl
import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.titleToSlug
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.util.Locale
import kotlinx.serialization.json.JsonPrimitive
typealias ApiPopularResponse = Data<PopularData>
typealias ApiSearchResponse = Data<SearchData>
typealias ApiMangaDetailsResponse = Data<MangaDetailsData>
typealias ApiChapterListResponse = Data<ChapterListData>
typealias ApiPageListResponse = Data<PageListData>
@Serializable
data class ApiPopularResponse(
val data: PopularResponseData,
) {
@Serializable
data class PopularResponseData(
@SerialName("queryPopular") val popular: PopularData,
) {
@Serializable
data class PopularData(
@SerialName("recommendations") val mangas: List<Popular>,
) {
@Serializable
data class Popular(
data class Data<T>(val data: T)
@Serializable
data class Edges<T>(val edges: List<T>)
// Popular
@Serializable
data class PopularData(
@SerialName("queryPopular") val popular: PopularMangas,
)
@Serializable
data class PopularMangas(
@SerialName("recommendations") val mangas: List<PopularManga>,
)
@Serializable
data class PopularManga(
@SerialName("anyCard") val manga: SearchManga? = null,
)
}
}
}
)
// Search
@Serializable
data class ApiSearchResponse(
val data: SearchResponseData,
) {
@Serializable
data class SearchResponseData(
val mangas: SearchResultMangas,
) {
@Serializable
data class SearchResultMangas(
@SerialName("edges") val mangas: List<SearchManga>,
)
}
}
data class SearchData(
val mangas: Edges<SearchManga>,
)
@Serializable
data class SearchManga(
@ -47,25 +55,22 @@ data class SearchManga(
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)
fun toSManga() = SManga.create().apply {
title = englishName ?: name
url = "/manga/$id/${name.titleToSlug()}"
thumbnail_url = thumbnail?.parseThumbnailUrl()
}
}
// Details
@Serializable
data class ApiMangaDetailsResponse(
val data: MangaDetailsData,
) {
@Serializable
data class MangaDetailsData(
data class MangaDetailsData(
val manga: Manga,
) {
@Serializable
data class Manga(
)
@Serializable
data class Manga(
@SerialName("_id") val id: String,
val name: String,
val thumbnail: String? = null,
@ -76,10 +81,9 @@ data class ApiMangaDetailsResponse(
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)
) {
fun toSManga() = SManga.create().apply {
title = englishName ?: name
url = "/manga/$id/${name.titleToSlug()}"
thumbnail_url = thumbnail?.parseThumbnailUrl()
description = this@Manga.description?.parseDescription()
@ -96,115 +100,56 @@ data class ApiMangaDetailsResponse(
author = authors.first().trim()
artist = author
}
genre = "${genres?.joinToString { it.trim() }}, ${tags?.joinToString { it.trim() }}"
genre = ((genres ?: emptyList()) + (tags ?: emptyList()))
.joinToString { it.trim() }
status = this@Manga.status.parseStatus()
}
}
}
}
// chapters details
@Serializable
data class ApiChapterListResponse(
val data: ChapterListData,
) {
@Serializable
data class ChapterListData(
val manga: ChapterList,
) {
@Serializable
data class ChapterList(
@SerialName("availableChaptersDetail") val chapters: AvailableChapters,
) {
@Serializable
data class AvailableChapters(
val sub: List<String>? = null,
)
}
}
}
@Serializable
data class ApiChapterListDetailsResponse(
val data: ChapterListData,
) {
@Serializable
data class ChapterListData(
data class ChapterListData(
@SerialName("episodeInfos") val chapterList: List<ChapterData>? = emptyList(),
) {
@Serializable
data class ChapterData(
@SerialName("episodeIdNum") val chapterNum: Float,
)
@Serializable
data class ChapterData(
@SerialName("episodeIdNum") val chapterNum: JsonPrimitive,
@SerialName("notes") val title: String? = null,
val uploadDates: DateDto? = null,
) {
@Serializable
data class DateDto(
val sub: String? = null,
)
) {
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
name = "Chapter $chapterNum"
if (!title.isNullOrEmpty() && !title.contains(numberRegex)) {
name += ": $title"
}
url = "/read/$mangaUrl/chapter-$chapterNum-sub"
date_upload = uploadDates?.sub.parseDate()
}
companion object {
private val numberRegex by lazy { Regex("\\d") }
}
}
@Serializable
data class ApiPageListResponse(
val data: PageListData,
) {
@Serializable
data class PageListData(
@SerialName("chapterPages") val pageList: PageList?,
) {
@Serializable
data class PageList(
@SerialName("edges") val serverList: List<Servers>?,
) {
@Serializable
data class Servers(
data class DateDto(
val sub: String? = null,
)
// page lsit
@Serializable
data class PageListData(
@SerialName("chapterPages") val pageList: Edges<Servers>?,
)
@Serializable
data class Servers(
@SerialName("pictureUrlHead") val serverUrl: String? = null,
val pictureUrls: List<PageUrl>?,
) {
@Serializable
data class PageUrl(
)
@Serializable
data class PageUrl(
val url: String,
)
}
}
}
}
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

@ -3,91 +3,30 @@ package eu.kanade.tachiyomi.extension.en.allanime
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
abstract class SelectFilter(name: String, private val options: List<Pair<String, String>>) :
Filter.Select<String>(name, options.map { it.first }.toTypedArray()) {
fun getValue() = options[state].second.takeUnless { it.isEmpty() }
}
internal class SortFilter(name: String, sorts: List<Pair<String, String>>) : SelectFilter(name, sorts)
internal class CountryFilter(name: String, countries: List<Pair<String, String>>) : SelectFilter(name, countries)
internal class Genre(name: String) : Filter.TriState(name)
internal class CountryFilter(name: String, private val countries: List<Pair<String, String>>) :
Filter.Select<String>(name, countries.map { it.first }.toTypedArray()) {
fun getValue() = countries[state].second
internal class GenreFilter(title: String, genres: List<String>) :
Filter.Group<Genre>(title, genres.map(::Genre)) {
val included: List<String>?
get() = state.filter { it.isIncluded() }.map { it.name }.takeUnless { it.isEmpty() }
val excluded: List<String>?
get() = state.filter { it.isExcluded() }.map { it.name }.takeUnless { it.isEmpty() }
}
internal class GenreFilter(title: String, genres: List<Genre>) :
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"),
Genre("Action"),
Genre("Adult"),
Genre("Adventure"),
Genre("Cars"),
Genre("Comedy"),
Genre("Cooking"),
Genre("Crossdressing"),
Genre("Dementia"),
Genre("Demons"),
Genre("Doujinshi"),
Genre("Drama"),
Genre("Ecchi"),
Genre("Fantasy"),
Genre("Game"),
Genre("Gender Bender"),
Genre("Gyaru"),
Genre("Harem"),
Genre("Historical"),
Genre("Horror"),
Genre("Isekai"),
Genre("Josei"),
Genre("Kids"),
Genre("Loli"),
Genre("Magic"),
Genre("Manhua"),
Genre("Manhwa"),
Genre("Martial Arts"),
Genre("Mature"),
Genre("Mecha"),
Genre("Medical"),
Genre("Military"),
Genre("Monster Girls"),
Genre("Music"),
Genre("Mystery"),
Genre("One Shot"),
Genre("Parody"),
Genre("Police"),
Genre("Post Apocalyptic"),
Genre("Psychological"),
Genre("Reincarnation"),
Genre("Reverse Harem"),
Genre("Romance"),
Genre("Samurai"),
Genre("School"),
Genre("Sci-Fi"),
Genre("Seinen"),
Genre("Shota"),
Genre("Shoujo"),
Genre("Shoujo Ai"),
Genre("Shounen"),
Genre("Shounen Ai"),
Genre("Slice of Life"),
Genre("Smut"),
Genre("Space"),
Genre("Sports"),
Genre("Super Power"),
Genre("Supernatural"),
Genre("Suspense"),
Genre("Thriller"),
Genre("Tragedy"),
Genre("Unknown"),
Genre("Vampire"),
Genre("Webtoons"),
Genre("Yaoi"),
Genre("Youkai"),
Genre("Yuri"),
Genre("Zombies"),
private val sortList = listOf(
Pair("Update", ""),
Pair("Name Ascending", "Name_ASC"),
Pair("Name Descending", "Name_DESC"),
)
private val countryList: List<Pair<String, String>> = listOf(
@ -97,7 +36,79 @@ private val countryList: List<Pair<String, String>> = listOf(
Pair("Korea", "KR"),
)
private val genreList: List<String> = listOf(
"4 Koma",
"Action",
"Adult",
"Adventure",
"Cars",
"Comedy",
"Cooking",
"Crossdressing",
"Dementia",
"Demons",
"Doujinshi",
"Drama",
"Ecchi",
"Fantasy",
"Game",
"Gender Bender",
"Gyaru",
"Harem",
"Historical",
"Horror",
"Isekai",
"Josei",
"Kids",
"Loli",
"Magic",
"Manhua",
"Manhwa",
"Martial Arts",
"Mature",
"Mecha",
"Medical",
"Military",
"Monster Girls",
"Music",
"Mystery",
"One Shot",
"Parody",
"Police",
"Post Apocalyptic",
"Psychological",
"Reincarnation",
"Reverse Harem",
"Romance",
"Samurai",
"School",
"Sci-Fi",
"Seinen",
"Shota",
"Shoujo",
"Shoujo Ai",
"Shounen",
"Shounen Ai",
"Slice of Life",
"Smut",
"Space",
"Sports",
"Super Power",
"Supernatural",
"Suspense",
"Thriller",
"Tragedy",
"Unknown",
"Vampire",
"Webtoons",
"Yaoi",
"Youkai",
"Yuri",
"Zombies",
)
fun getFilters() = FilterList(
SortFilter("Sort", sortList),
CountryFilter("Countries", countryList),
GenreFilter("Genres", genreList),
)

View File

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.extension.en.allanime
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.Jsoup
import java.text.SimpleDateFormat
import java.util.Locale
object AllAnimeHelper {
val json: Json = Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
coerceInputValues = true
}
fun String.parseThumbnailUrl(): String {
return if (this.matches(AllAnime.urlRegex)) {
this
} else {
"$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
}
}
fun String.titleToSlug() = this.trim()
.lowercase(Locale.US)
.replace(titleSpecialCharactersRegex, "-")
fun String.parseDescription(): String {
return Jsoup.parse(
this.replace("<br>", "br2n"),
).text().replace("br2n", "\n")
}
fun String?.parseDate(): Long {
return runCatching {
dateFormat.parse(this!!)!!.time
}.getOrDefault(0L)
}
inline fun <reified T> Response.parseAs(): T = json.decodeFromString(body.string())
inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
filterIsInstance<T>().firstOrNull()
inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =
json.encodeToString(this)
.toRequestBody(JSON_MEDIA_TYPE)
fun Headers.Builder.buildApiHeaders(requestBody: RequestBody) = this
.add("Content-Length", requestBody.contentLength().toString())
.add("Content-Type", requestBody.contentType().toString())
.build()
private const val thumbnail_cdn = "https://wp.youtube-anime.com/aln.youtube-anime.com/"
private val titleSpecialCharactersRegex by lazy { Regex("[^a-z\\d]+") }
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
}

View File

@ -1,163 +1,59 @@
package eu.kanade.tachiyomi.extension.en.allanime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ApiPopularPayload(
val variables: ApiPopularVariables,
data class GraphQL<T>(
val variables: T,
val query: String,
) {
@Serializable
data class ApiPopularVariables(
)
@Serializable
data class PopularVariables(
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(
data class SearchVariables(
val search: SearchPayload,
val limit: Int,
@SerialName("limit") val size: Int,
val page: Int,
val translationType: String,
val countryOrigin: String,
)
)
@Serializable
data class SearchPayload(
val query: String,
@Serializable
data class SearchPayload(
val query: String?,
val sortBy: 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(
data class IDVariables(
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(
data class ChapterListVariables(
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(
data class PageListVariables(
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,11 +1,8 @@
package eu.kanade.tachiyomi.extension.en.allanime
import eu.kanade.tachiyomi.extension.en.allanime.AllAnime.Companion.whitespace
private fun buildQuery(queryAction: () -> String): String {
return queryAction()
.trimIndent()
.replace(whitespace, " ")
.replace("%", "$")
}
@ -33,7 +30,6 @@ val POPULAR_QUERY: String = buildQuery {
name
thumbnail
englishName
nativeName
}
}
}
@ -62,7 +58,6 @@ val SEARCH_QUERY: String = buildQuery {
name
thumbnail
englishName
nativeName
}
}
}
@ -83,23 +78,12 @@ val DETAILS_QUERY: String = buildQuery {
status
altNames
englishName
nativeName
}
}
"""
}
val CHAPTERS_QUERY: String = buildQuery {
"""
query (%id: String!) {
manga(_id: %id) {
availableChaptersDetail
}
}
"""
}
val CHAPTERS_DETAILS_QUERY: String = buildQuery {
"""
query (%id: String!, %chapterNumStart: Float!, %chapterNumEnd: Float!) {
episodeInfos(