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(