diff --git a/src/en/allanime/AndroidManifest.xml b/src/en/allanime/AndroidManifest.xml
index 8ce8c57a6..4006c521b 100644
--- a/src/en/allanime/AndroidManifest.xml
+++ b/src/en/allanime/AndroidManifest.xml
@@ -13,15 +13,10 @@
-
-
-
-
-
+
+
+
+
diff --git a/src/en/allanime/build.gradle b/src/en/allanime/build.gradle
index c3a5b0e58..6003d4863 100644
--- a/src/en/allanime/build.gradle
+++ b/src/en/allanime/build.gradle
@@ -6,7 +6,7 @@ ext {
extName = 'AllAnime'
pkgNameSuffix = 'en.allanime'
extClass = '.AllAnime'
- extVersionCode = 5
+ extVersionCode = 6
}
apply from: "$rootDir/common.gradle"
diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnime.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnime.kt
index b13786d0b..97e530a5a 100644
--- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnime.kt
+++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnime.kt
@@ -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().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().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(
- size = limit,
- dateRange = 0,
- page = page,
- allowAdult = preferences.allowAdult,
+ 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()
- 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,
- genres = filters.firstInstanceOrNull()?.included,
- excludeGenres = filters.firstInstanceOrNull()?.excluded,
- translationType = "sub",
- countryOrigin = filters.firstInstanceOrNull()?.getValue() ?: "ALL",
- allowAdult = preferences.allowAdult,
+ val payload = GraphQL(
+ SearchVariables(
+ search = SearchPayload(
+ query = query.takeUnless { it.isEmpty() },
+ sortBy = filters.firstInstanceOrNull()?.getValue(),
+ genres = filters.firstInstanceOrNull()?.included,
+ excludeGenres = filters.firstInstanceOrNull()?.excluded,
+ isManga = true,
+ allowAdult = preferences.allowAdult,
+ allowUnknown = false,
+ ),
+ size = limit,
+ page = page,
+ translationType = "sub",
+ countryOrigin = filters.firstInstanceOrNull()?.getValue() ?: "ALL",
+ ),
+ 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()
- 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()
- 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 {
val result = response.parseAs()
- 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()
- }.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 {
@@ -222,56 +230,38 @@ class AllAnime : ConfigurableSource, HttpSource() {
}
/* Pages */
- override fun fetchPageList(chapter: SChapter): Observable> {
- 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(
- id = mangaId,
- chapterNum = chapterNo,
- translationType = "sub",
+ 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 {
- val result = json.decodeFromString(response.body.string())
- val pages = result.data.pageList?.serverList?.get(0) ?: return emptyList()
+ override fun pageListParse(response: Response): List {
+ val result = response.parseAs()
+ val pages = result.data.pageList?.edges?.get(0) ?: return emptyList()
- val imageDomain = if (!pages.serverUrl.isNullOrEmpty()) {
- pages.serverUrl.let { server ->
- if (server.matches(urlRegex)) {
- server
- } else {
- "https://$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 {
- throw UnsupportedOperationException("Not used")
- }
-
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException("Not used")
}
- /* Helpers */
- private inline fun 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 Response.parseAs(): T = json.decodeFromString(body.string())
-
- private inline fun List<*>.firstInstanceOrNull(): R? =
- filterIsInstance().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"
diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeDto.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeDto.kt
index db5dabcef..3c6ffd281 100644
--- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeDto.kt
+++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeDto.kt
@@ -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
+
+typealias ApiSearchResponse = Data
+
+typealias ApiMangaDetailsResponse = Data
+
+typealias ApiChapterListResponse = Data
+
+typealias ApiPageListResponse = Data
@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,
- ) {
- @Serializable
- data class Popular(
- @SerialName("anyCard") val manga: SearchManga? = null,
- )
- }
- }
-}
+data class Data(val data: T)
@Serializable
-data class ApiSearchResponse(
- val data: SearchResponseData,
-) {
- @Serializable
- data class SearchResponseData(
- val mangas: SearchResultMangas,
- ) {
- @Serializable
- data class SearchResultMangas(
- @SerialName("edges") val mangas: List,
- )
- }
-}
+data class Edges(val edges: List)
+
+// Popular
+@Serializable
+data class PopularData(
+ @SerialName("queryPopular") val popular: PopularMangas,
+)
+
+@Serializable
+data class PopularMangas(
+ @SerialName("recommendations") val mangas: List,
+)
+
+@Serializable
+data class PopularManga(
+ @SerialName("anyCard") val manga: SearchManga? = null,
+)
+
+// Search
+@Serializable
+data class SearchData(
+ val mangas: Edges,
+)
@Serializable
data class SearchManga(
@@ -47,164 +55,101 @@ 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(
- 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? = emptyList(),
- val genres: List? = emptyList(),
- val tags: List? = emptyList(),
- val status: String? = null,
- val altNames: List? = 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()) {
- if (description.isNullOrEmpty()) {
- description = "Alternative Titles:\n"
- } else {
- description += "\n\nAlternative Titles:\n"
- }
+data class MangaDetailsData(
+ val manga: Manga,
+)
- description += altNames.joinToString("\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(
+ @SerialName("_id") val id: String,
+ val name: String,
+ val thumbnail: String? = null,
+ val description: String? = null,
+ val authors: List? = emptyList(),
+ val genres: List? = emptyList(),
+ val tags: List? = emptyList(),
+ val status: String? = null,
+ val altNames: List? = emptyList(),
+ val englishName: String? = null,
+) {
+ fun toSManga() = SManga.create().apply {
+ title = englishName ?: name
+ url = "/manga/$id/${name.titleToSlug()}"
+ thumbnail_url = thumbnail?.parseThumbnailUrl()
+ description = this@Manga.description?.parseDescription()
+ if (!altNames.isNullOrEmpty()) {
+ if (description.isNullOrEmpty()) {
+ description = "Alternative Titles:\n"
+ } else {
+ description += "\n\nAlternative Titles:\n"
}
+
+ description += altNames.joinToString("\n") { "• ${it.trim()}" }
}
+ if (authors?.isNotEmpty() == true) {
+ author = authors.first().trim()
+ artist = author
+ }
+ genre = ((genres ?: emptyList()) + (tags ?: emptyList()))
+ .joinToString { it.trim() }
+ status = this@Manga.status.parseStatus()
+ }
+}
+
+// chapters details
+@Serializable
+data class ChapterListData(
+ @SerialName("episodeInfos") val chapterList: List? = emptyList(),
+)
+
+@Serializable
+data class ChapterData(
+ @SerialName("episodeIdNum") val chapterNum: JsonPrimitive,
+ @SerialName("notes") val title: String? = null,
+ val uploadDates: DateDto? = 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 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? = null,
- )
- }
- }
-}
+data class DateDto(
+ val sub: String? = null,
+)
+
+// page lsit
+@Serializable
+data class PageListData(
+ @SerialName("chapterPages") val pageList: Edges?,
+)
@Serializable
-data class ApiChapterListDetailsResponse(
- val data: ChapterListData,
-) {
- @Serializable
- data class ChapterListData(
- @SerialName("episodeInfos") val chapterList: List? = 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,
- )
- }
- }
-}
+data class Servers(
+ @SerialName("pictureUrlHead") val serverUrl: String? = null,
+ val pictureUrls: List?,
+)
@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?,
- ) {
- @Serializable
- data class Servers(
- @SerialName("pictureUrlHead") val serverUrl: String? = null,
- val pictureUrls: List?,
- ) {
- @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("
", "br2n"),
- ).text().replace("br2n", "\n")
-}
+data class PageUrl(
+ val url: String,
+)
diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeFiters.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeFiters.kt
index 60b0eeef0..4dce4326e 100644
--- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeFiters.kt
+++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeFiters.kt
@@ -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>) :
+ Filter.Select(name, options.map { it.first }.toTypedArray()) {
+ fun getValue() = options[state].second.takeUnless { it.isEmpty() }
+}
+
+internal class SortFilter(name: String, sorts: List>) : SelectFilter(name, sorts)
+
+internal class CountryFilter(name: String, countries: List>) : SelectFilter(name, countries)
+
internal class Genre(name: String) : Filter.TriState(name)
-internal class CountryFilter(name: String, private val countries: List>) :
- Filter.Select(name, countries.map { it.first }.toTypedArray()) {
- fun getValue() = countries[state].second
+internal class GenreFilter(title: String, genres: List) :
+ Filter.Group(title, genres.map(::Genre)) {
+ val included: List?
+ get() = state.filter { it.isIncluded() }.map { it.name }.takeUnless { it.isEmpty() }
+
+ val excluded: List?
+ get() = state.filter { it.isExcluded() }.map { it.name }.takeUnless { it.isEmpty() }
}
-internal class GenreFilter(title: String, genres: List) :
- Filter.Group(title, genres) {
- val included: List
- get() = state.filter { it.isIncluded() }.map { it.name }
-
- val excluded: List
- get() = state.filter { it.isExcluded() }.map { it.name }
-}
-
-private val genreList: List = 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> = listOf(
@@ -97,7 +36,79 @@ private val countryList: List> = listOf(
Pair("Korea", "KR"),
)
+private val genreList: List = 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),
)
diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeHelper.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeHelper.kt
new file mode 100644
index 000000000..f579c4493
--- /dev/null
+++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeHelper.kt
@@ -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("
", "br2n"),
+ ).text().replace("br2n", "\n")
+ }
+
+ fun String?.parseDate(): Long {
+ return runCatching {
+ dateFormat.parse(this!!)!!.time
+ }.getOrDefault(0L)
+ }
+
+ inline fun Response.parseAs(): T = json.decodeFromString(body.string())
+
+ inline fun List<*>.firstInstanceOrNull(): T? =
+ filterIsInstance().firstOrNull()
+
+ inline fun 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()
+}
diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimePayloadDto.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimePayloadDto.kt
index c481290a6..7f53253a1 100644
--- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimePayloadDto.kt
+++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimePayloadDto.kt
@@ -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(
+ val variables: T,
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?,
- val excludeGenres: List?,
- val isManga: Boolean,
- val allowAdult: Boolean,
- val allowUnknown: Boolean,
- )
-
- constructor(
- query: String,
- size: Int,
- page: Int,
- genres: List?,
- excludeGenres: List?,
- 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,
- )
-}
+data class PopularVariables(
+ val type: String,
+ val size: Int,
+ val dateRange: Int,
+ val page: Int,
+ val allowAdult: Boolean,
+ val allowUnknown: Boolean,
+)
@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,
- )
-}
+data class SearchVariables(
+ val search: SearchPayload,
+ @SerialName("limit") val size: Int,
+ val page: Int,
+ val translationType: String,
+ val countryOrigin: String,
+)
@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,
- )
-}
+data class SearchPayload(
+ val query: String?,
+ val sortBy: String?,
+ val genres: List?,
+ val excludeGenres: List?,
+ val isManga: Boolean,
+ val allowAdult: Boolean,
+ val allowUnknown: Boolean,
+)
@Serializable
-data class ApiPageListPayload(
- val variables: ApiPageListVariables,
- val query: String,
-) {
- @Serializable
- data class ApiPageListVariables(
- val id: String,
- val chapterNum: String,
- val translationType: String,
- )
+data class IDVariables(
+ val id: String,
+)
- constructor(
- id: String,
- chapterNum: String,
- translationType: String,
- ) : this(
- ApiPageListVariables(
- id = id,
- chapterNum = chapterNum,
- translationType = translationType,
- ),
- PAGE_QUERY,
- )
-}
+@Serializable
+data class ChapterListVariables(
+ val id: String,
+ val chapterNumStart: Float,
+ val chapterNumEnd: Float,
+)
+
+@Serializable
+data class PageListVariables(
+ val id: String,
+ val chapterNum: String,
+ val translationType: String,
+)
diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeQueries.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeQueries.kt
index 054503b2b..2128947b7 100644
--- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeQueries.kt
+++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeQueries.kt
@@ -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(