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.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:host="allanime.to"/> <data android:host="allanime.ai"/>
<data android:host="allanime.co"/> <data android:scheme="https"/>
<data android:pathPattern="/manga/..*"/>
<data <data android:pathPattern="/read/..*"/>
android:pathPattern="/manga/..*"
android:scheme="https" />
<data
android:pathPattern="/read/..*"
android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

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

View File

@ -5,7 +5,10 @@ import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat 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.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.json.float
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class AllAnime : ConfigurableSource, HttpSource() { class AllAnime : ConfigurableSource, HttpSource() {
override val name = "AllAnime" 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" override val baseUrl = "https://allanime.ai"
private val apiUrl = "https://api.allanime.day/api" 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 -> .addInterceptor { chain ->
val request = chain.request() val request = chain.request()
val frag = request.url.fragment val frag = request.url.fragment
@ -80,24 +69,34 @@ class AllAnime : ConfigurableSource, HttpSource() {
/* Popular */ /* Popular */
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val payloadObj = ApiPopularPayload( val payload = GraphQL(
size = limit, PopularVariables(
dateRange = 0, type = "manga",
page = page, size = limit,
allowAdult = preferences.allowAdult, 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 { override fun popularMangaParse(response: Response): MangasPage {
val result = response.parseAs<ApiPopularResponse>() val result = response.parseAs<ApiPopularResponse>()
val titleStyle = preferences.titlePref
val mangaList = result.data.popular.mangas 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 */ /* Latest */
@ -118,28 +117,41 @@ class AllAnime : ConfigurableSource, HttpSource() {
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val payloadObj = ApiSearchPayload( val payload = GraphQL(
query = query, SearchVariables(
size = limit, search = SearchPayload(
page = page, query = query.takeUnless { it.isEmpty() },
genres = filters.firstInstanceOrNull<GenreFilter>()?.included, sortBy = filters.firstInstanceOrNull<SortFilter>()?.getValue(),
excludeGenres = filters.firstInstanceOrNull<GenreFilter>()?.excluded, genres = filters.firstInstanceOrNull<GenreFilter>()?.included,
translationType = "sub", excludeGenres = filters.firstInstanceOrNull<GenreFilter>()?.excluded,
countryOrigin = filters.firstInstanceOrNull<CountryFilter>()?.getValue() ?: "ALL", isManga = true,
allowAdult = preferences.allowAdult, allowAdult = preferences.allowAdult,
allowUnknown = false,
),
size = limit,
page = page,
translationType = "sub",
countryOrigin = filters.firstInstanceOrNull<CountryFilter>()?.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 { override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<ApiSearchResponse>() val result = response.parseAs<ApiSearchResponse>()
val titleStyle = preferences.titlePref
val mangaList = result.data.mangas.mangas val mangaList = result.data.mangas.edges
.map { it.toSManga(titleStyle) } .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() override fun getFilterList() = getFilters()
@ -147,15 +159,23 @@ class AllAnime : ConfigurableSource, HttpSource() {
/* Details */ /* Details */
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
val mangaId = manga.url.split("/")[2] val mangaId = manga.url.split("/")[2]
val payloadObj = ApiIDPayload(mangaId, DETAILS_QUERY)
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 { override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<ApiMangaDetailsResponse>() val result = response.parseAs<ApiMangaDetailsResponse>()
return result.data.manga.toSManga(preferences.titlePref) return result.data.manga.toSManga()
} }
override fun getMangaUrl(manga: SManga): String { override fun getMangaUrl(manga: SManga): String {
@ -173,44 +193,32 @@ class AllAnime : ConfigurableSource, HttpSource() {
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
val mangaId = manga.url.split("/")[2] val mangaId = manga.url.split("/")[2]
val payloadObj = ApiIDPayload(mangaId, CHAPTERS_QUERY)
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 requestBody = payload.toJsonRequestBody()
val mangaId = manga.url.split("/")[2]
val payloadObj = ApiChapterListDetailsPayload(mangaId, start.toFloat(), end.toFloat())
return apiRequest(payloadObj) val apiHeaders = headersBuilder().buildApiHeaders(requestBody)
return POST(apiUrl, apiHeaders, requestBody)
} }
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> { private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val result = response.parseAs<ApiChapterListResponse>() val result = response.parseAs<ApiChapterListResponse>()
val chapters = result.data.manga.chapters.sub
?.sortedBy { it.toFloat() }
?: return emptyList()
val chapterDetails = client.newCall( val chapters = result.data.chapterList?.sortedByDescending { it.chapterNum.float }
chapterDetailsRequest(manga, chapters.first(), chapters.last()), ?: return emptyList()
).execute()
.use {
it.parseAs<ApiChapterListDetailsResponse>()
}.data.chapterList
?.sortedBy { it.chapterNum }
val mangaUrl = manga.url.substringAfter("/manga/") val mangaUrl = manga.url.substringAfter("/manga/")
return chapterDetails?.zip(chapters)?.map { (details, chapterNum) -> return chapters.map { it.toSChapter(mangaUrl) }
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()
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
@ -222,56 +230,38 @@ class AllAnime : ConfigurableSource, HttpSource() {
} }
/* Pages */ /* 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 { override fun pageListRequest(chapter: SChapter): Request {
val chapterUrl = chapter.url.split("/") val chapterUrl = chapter.url.split("/")
val mangaId = chapterUrl[2] val mangaId = chapterUrl[2]
val chapterNo = chapterUrl[4].split("-")[1] val chapterNo = chapterUrl[4].split("-")[1]
val payloadObj = ApiPageListPayload( val payload = GraphQL(
id = mangaId, PageListVariables(
chapterNum = chapterNo, id = mangaId,
translationType = "sub", 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> { override fun pageListParse(response: Response): List<Page> {
val result = json.decodeFromString<ApiPageListResponse>(response.body.string()) val result = response.parseAs<ApiPageListResponse>()
val pages = result.data.pageList?.serverList?.get(0) ?: return emptyList() val pages = result.data.pageList?.edges?.get(0) ?: return emptyList()
val imageDomain = if (!pages.serverUrl.isNullOrEmpty()) { val imageDomain = pages.serverUrl?.let { server ->
pages.serverUrl.let { server -> if (server.matches(urlRegex)) {
if (server.matches(urlRegex)) { server
server } else {
} else { "https://$server"
"https://$server"
}
} }
} else { } ?: return emptyList()
// 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 pages.pictureUrls?.mapIndexed { index, image -> return pages.pictureUrls?.mapIndexed { index, image ->
Page( Page(
@ -281,38 +271,10 @@ class AllAnime : ConfigurableSource, HttpSource() {
} ?: emptyList() } ?: emptyList()
} }
override fun pageListParse(response: Response): List<Page> {
throw UnsupportedOperationException("Not used")
}
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException("Not used") 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) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = IMAGE_QUALITY_PREF key = IMAGE_QUALITY_PREF
@ -321,27 +283,15 @@ class AllAnime : ConfigurableSource, HttpSource() {
entryValues = arrayOf("original", "800", "480") entryValues = arrayOf("original", "800", "480")
setDefaultValue(IMAGE_QUALITY_PREF_DEFAULT) setDefaultValue(IMAGE_QUALITY_PREF_DEFAULT)
summary = "Warning: Wp quality servers can be slow and might not work sometimes" summary = "Warning: Wp quality servers can be slow and might not work sometimes"
}.let { screen.addPreference(it) } }.also(screen::addPreference)
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) }
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ADULT_PREF key = SHOW_ADULT_PREF
title = "Show Adult Content" title = "Show Adult Content"
setDefaultValue(SHOW_ADULT_PREF_DEFAULT) 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 private val SharedPreferences.allowAdult
get() = getBoolean(SHOW_ADULT_PREF, SHOW_ADULT_PREF_DEFAULT) get() = getBoolean(SHOW_ADULT_PREF, SHOW_ADULT_PREF_DEFAULT)
@ -350,22 +300,11 @@ class AllAnime : ConfigurableSource, HttpSource() {
companion object { companion object {
private const val limit = 20 private const val limit = 20
private val numberRegex by lazy { Regex("\\d") }
val whitespace by lazy { Regex("\\s+") }
val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
const val SEARCH_PREFIX = "id:" const val SEARCH_PREFIX = "id:"
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?://.*") val urlRegex = Regex("^https?://.*")
private const val image_cdn = "https://wp.youtube-anime.com" private const val image_cdn = "https://wp.youtube-anime.com"
private val imageQualityRegex = Regex("^https?://(.*)#.*") 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 = "pref_adult"
private const val SHOW_ADULT_PREF_DEFAULT = false private const val SHOW_ADULT_PREF_DEFAULT = false
private const val IMAGE_QUALITY_PREF = "pref_quality" private const val IMAGE_QUALITY_PREF = "pref_quality"

View File

@ -1,45 +1,53 @@
package eu.kanade.tachiyomi.extension.en.allanime 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 eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import kotlinx.serialization.json.JsonPrimitive
import java.util.Locale
typealias ApiPopularResponse = Data<PopularData>
typealias ApiSearchResponse = Data<SearchData>
typealias ApiMangaDetailsResponse = Data<MangaDetailsData>
typealias ApiChapterListResponse = Data<ChapterListData>
typealias ApiPageListResponse = Data<PageListData>
@Serializable @Serializable
data class ApiPopularResponse( data class Data<T>(val data: T)
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(
@SerialName("anyCard") val manga: SearchManga? = null,
)
}
}
}
@Serializable @Serializable
data class ApiSearchResponse( data class Edges<T>(val edges: List<T>)
val data: SearchResponseData,
) { // Popular
@Serializable @Serializable
data class SearchResponseData( data class PopularData(
val mangas: SearchResultMangas, @SerialName("queryPopular") val popular: PopularMangas,
) { )
@Serializable
data class SearchResultMangas( @Serializable
@SerialName("edges") val mangas: List<SearchManga>, data class PopularMangas(
) @SerialName("recommendations") val mangas: List<PopularManga>,
} )
}
@Serializable
data class PopularManga(
@SerialName("anyCard") val manga: SearchManga? = null,
)
// Search
@Serializable
data class SearchData(
val mangas: Edges<SearchManga>,
)
@Serializable @Serializable
data class SearchManga( data class SearchManga(
@ -47,164 +55,101 @@ data class SearchManga(
val name: String, val name: String,
val thumbnail: String? = null, val thumbnail: String? = null,
val englishName: String? = null, val englishName: String? = null,
val nativeName: String? = null,
) { ) {
fun toSManga(titleStyle: String?) = SManga.create().apply { fun toSManga() = SManga.create().apply {
title = titleStyle.preferedName(name, englishName, nativeName) title = englishName ?: name
url = "/manga/$id/${name.titleToSlug()}" url = "/manga/$id/${name.titleToSlug()}"
thumbnail_url = thumbnail?.parseThumbnailUrl() thumbnail_url = thumbnail?.parseThumbnailUrl()
} }
} }
// Details
@Serializable @Serializable
data class ApiMangaDetailsResponse( data class MangaDetailsData(
val data: MangaDetailsData, val manga: Manga,
) { )
@Serializable
data class MangaDetailsData(
val manga: Manga,
) {
@Serializable
data class Manga(
@SerialName("_id") val id: String,
val name: String,
val thumbnail: String? = null,
val description: String? = null,
val authors: List<String>? = emptyList(),
val genres: List<String>? = emptyList(),
val tags: List<String>? = emptyList(),
val status: String? = null,
val altNames: List<String>? = emptyList(),
val englishName: String? = null,
val nativeName: String? = null,
) {
fun toSManga(titleStyle: String?) = SManga.create().apply {
title = titleStyle.preferedName(name, englishName, nativeName)
url = "/manga/$id/${name.titleToSlug()}"
thumbnail_url = thumbnail?.parseThumbnailUrl()
description = this@Manga.description?.parseDescription()
if (!altNames.isNullOrEmpty()) {
if (description.isNullOrEmpty()) {
description = "Alternative Titles:\n"
} else {
description += "\n\nAlternative Titles:\n"
}
description += altNames.joinToString("\n") { "${it.trim()}" } @Serializable
} data class Manga(
if (authors?.isNotEmpty() == true) { @SerialName("_id") val id: String,
author = authors.first().trim() val name: String,
artist = author val thumbnail: String? = null,
} val description: String? = null,
genre = "${genres?.joinToString { it.trim() }}, ${tags?.joinToString { it.trim() }}" val authors: List<String>? = emptyList(),
status = this@Manga.status.parseStatus() val genres: List<String>? = emptyList(),
val tags: List<String>? = emptyList(),
val status: String? = null,
val altNames: List<String>? = 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<ChapterData>? = 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 @Serializable
data class ApiChapterListResponse( data class DateDto(
val data: ChapterListData, val sub: String? = null,
) { )
@Serializable
data class ChapterListData( // page lsit
val manga: ChapterList, @Serializable
) { data class PageListData(
@Serializable @SerialName("chapterPages") val pageList: Edges<Servers>?,
data class ChapterList( )
@SerialName("availableChaptersDetail") val chapters: AvailableChapters,
) {
@Serializable
data class AvailableChapters(
val sub: List<String>? = null,
)
}
}
}
@Serializable @Serializable
data class ApiChapterListDetailsResponse( data class Servers(
val data: ChapterListData, @SerialName("pictureUrlHead") val serverUrl: String? = null,
) { val pictureUrls: List<PageUrl>?,
@Serializable )
data class ChapterListData(
@SerialName("episodeInfos") val chapterList: List<ChapterData>? = emptyList(),
) {
@Serializable
data class ChapterData(
@SerialName("episodeIdNum") val chapterNum: Float,
@SerialName("notes") val title: String? = null,
val uploadDates: DateDto? = null,
) {
@Serializable
data class DateDto(
val sub: String? = null,
)
}
}
}
@Serializable @Serializable
data class ApiPageListResponse( data class PageUrl(
val data: PageListData, val url: String,
) { )
@Serializable
data class PageListData(
@SerialName("chapterPages") val pageList: PageList?,
) {
@Serializable
data class PageList(
@SerialName("edges") val serverList: List<Servers>?,
) {
@Serializable
data class Servers(
@SerialName("pictureUrlHead") val serverUrl: String? = null,
val pictureUrls: List<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.Filter
import eu.kanade.tachiyomi.source.model.FilterList 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 Genre(name: String) : Filter.TriState(name)
internal class CountryFilter(name: String, private val countries: List<Pair<String, String>>) : internal class GenreFilter(title: String, genres: List<String>) :
Filter.Select<String>(name, countries.map { it.first }.toTypedArray()) { Filter.Group<Genre>(title, genres.map(::Genre)) {
fun getValue() = countries[state].second 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>) : private val sortList = listOf(
Filter.Group<Genre>(title, genres) { Pair("Update", ""),
val included: List<String> Pair("Name Ascending", "Name_ASC"),
get() = state.filter { it.isIncluded() }.map { it.name } Pair("Name Descending", "Name_DESC"),
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 countryList: List<Pair<String, String>> = listOf( private val countryList: List<Pair<String, String>> = listOf(
@ -97,7 +36,79 @@ private val countryList: List<Pair<String, String>> = listOf(
Pair("Korea", "KR"), 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( fun getFilters() = FilterList(
SortFilter("Sort", sortList),
CountryFilter("Countries", countryList), CountryFilter("Countries", countryList),
GenreFilter("Genres", genreList), 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 package eu.kanade.tachiyomi.extension.en.allanime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ApiPopularPayload( data class GraphQL<T>(
val variables: ApiPopularVariables, val variables: T,
val query: String, 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 @Serializable
data class ApiSearchPayload( data class PopularVariables(
val variables: ApiSearchVariables, val type: String,
val query: String, val size: Int,
) { val dateRange: Int,
@Serializable val page: Int,
data class ApiSearchVariables( val allowAdult: Boolean,
val search: SearchPayload, val allowUnknown: Boolean,
val limit: Int, )
val page: Int,
val translationType: String,
val countryOrigin: String,
)
@Serializable
data class SearchPayload(
val query: String,
val genres: List<String>?,
val excludeGenres: List<String>?,
val isManga: Boolean,
val allowAdult: Boolean,
val allowUnknown: Boolean,
)
constructor(
query: String,
size: Int,
page: Int,
genres: List<String>?,
excludeGenres: List<String>?,
translationType: String,
countryOrigin: String,
isManga: Boolean = true,
allowAdult: Boolean = false,
allowUnknown: Boolean = false,
) : this(
ApiSearchVariables(
search = SearchPayload(
query = query,
genres = genres,
excludeGenres = excludeGenres,
isManga = isManga,
allowAdult = allowAdult,
allowUnknown = allowUnknown,
),
limit = size,
page = page,
translationType = translationType,
countryOrigin = countryOrigin,
),
SEARCH_QUERY,
)
}
@Serializable @Serializable
data class ApiIDPayload( data class SearchVariables(
val variables: ApiIDVariables, val search: SearchPayload,
val query: String, @SerialName("limit") val size: Int,
) { val page: Int,
@Serializable val translationType: String,
data class ApiIDVariables( val countryOrigin: String,
val id: String, )
)
constructor(
id: String,
graphqlQuery: String,
) : this(
ApiIDVariables(id),
graphqlQuery,
)
}
@Serializable @Serializable
data class ApiChapterListDetailsPayload( data class SearchPayload(
val variables: ApiChapterDetailsVariables, val query: String?,
val query: String, val sortBy: String?,
) { val genres: List<String>?,
@Serializable val excludeGenres: List<String>?,
data class ApiChapterDetailsVariables( val isManga: Boolean,
val id: String, val allowAdult: Boolean,
val chapterNumStart: Float, val allowUnknown: Boolean,
val chapterNumEnd: Float, )
)
constructor(
id: String,
chapterNumStart: Float,
chapterNumEnd: Float,
) : this(
ApiChapterDetailsVariables(
id = "manga@$id",
chapterNumStart = chapterNumStart,
chapterNumEnd = chapterNumEnd,
),
CHAPTERS_DETAILS_QUERY,
)
}
@Serializable @Serializable
data class ApiPageListPayload( data class IDVariables(
val variables: ApiPageListVariables, val id: String,
val query: String, )
) {
@Serializable
data class ApiPageListVariables(
val id: String,
val chapterNum: String,
val translationType: String,
)
constructor( @Serializable
id: String, data class ChapterListVariables(
chapterNum: String, val id: String,
translationType: String, val chapterNumStart: Float,
) : this( val chapterNumEnd: Float,
ApiPageListVariables( )
id = id,
chapterNum = chapterNum, @Serializable
translationType = translationType, data class PageListVariables(
), val id: String,
PAGE_QUERY, val chapterNum: String,
) val translationType: String,
} )

View File

@ -1,11 +1,8 @@
package eu.kanade.tachiyomi.extension.en.allanime package eu.kanade.tachiyomi.extension.en.allanime
import eu.kanade.tachiyomi.extension.en.allanime.AllAnime.Companion.whitespace
private fun buildQuery(queryAction: () -> String): String { private fun buildQuery(queryAction: () -> String): String {
return queryAction() return queryAction()
.trimIndent() .trimIndent()
.replace(whitespace, " ")
.replace("%", "$") .replace("%", "$")
} }
@ -33,7 +30,6 @@ val POPULAR_QUERY: String = buildQuery {
name name
thumbnail thumbnail
englishName englishName
nativeName
} }
} }
} }
@ -62,7 +58,6 @@ val SEARCH_QUERY: String = buildQuery {
name name
thumbnail thumbnail
englishName englishName
nativeName
} }
} }
} }
@ -83,23 +78,12 @@ val DETAILS_QUERY: String = buildQuery {
status status
altNames altNames
englishName englishName
nativeName
} }
} }
""" """
} }
val CHAPTERS_QUERY: String = buildQuery { 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!) { query (%id: String!, %chapterNumStart: Float!, %chapterNumEnd: Float!) {
episodeInfos( episodeInfos(