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

* 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(
PopularVariables(
type = "manga",
size = limit, size = limit,
dateRange = 0, dateRange = 0,
page = page, page = page,
allowAdult = preferences.allowAdult, 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() },
sortBy = filters.firstInstanceOrNull<SortFilter>()?.getValue(),
genres = filters.firstInstanceOrNull<GenreFilter>()?.included, genres = filters.firstInstanceOrNull<GenreFilter>()?.included,
excludeGenres = filters.firstInstanceOrNull<GenreFilter>()?.excluded, excludeGenres = filters.firstInstanceOrNull<GenreFilter>()?.excluded,
isManga = true,
allowAdult = preferences.allowAdult,
allowUnknown = false,
),
size = limit,
page = page,
translationType = "sub", translationType = "sub",
countryOrigin = filters.firstInstanceOrNull<CountryFilter>()?.getValue() ?: "ALL", countryOrigin = filters.firstInstanceOrNull<CountryFilter>()?.getValue() ?: "ALL",
allowAdult = preferences.allowAdult, ),
SEARCH_QUERY,
) )
return apiRequest(payloadObj) val requestBody = payload.toJsonRequestBody()
val apiHeaders = headersBuilder().buildApiHeaders(requestBody)
return POST(apiUrl, apiHeaders, requestBody)
} }
override fun searchMangaParse(response: Response): MangasPage { 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(
PageListVariables(
id = mangaId, id = mangaId,
chapterNum = chapterNo, chapterNum = chapterNo,
translationType = "sub", 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"
} }
} } ?: return emptyList()
} 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 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
@Serializable data class Edges<T>(val edges: List<T>)
data class PopularResponseData(
@SerialName("queryPopular") val popular: PopularData, // Popular
) { @Serializable
@Serializable data class PopularData(
data class PopularData( @SerialName("queryPopular") val popular: PopularMangas,
@SerialName("recommendations") val mangas: List<Popular>, )
) {
@Serializable @Serializable
data class Popular( data class PopularMangas(
@SerialName("recommendations") val mangas: List<PopularManga>,
)
@Serializable
data class PopularManga(
@SerialName("anyCard") val manga: SearchManga? = null, @SerialName("anyCard") val manga: SearchManga? = null,
) )
}
}
}
// Search
@Serializable @Serializable
data class ApiSearchResponse( data class SearchData(
val data: SearchResponseData, val mangas: Edges<SearchManga>,
) { )
@Serializable
data class SearchResponseData(
val mangas: SearchResultMangas,
) {
@Serializable
data class SearchResultMangas(
@SerialName("edges") val mangas: List<SearchManga>,
)
}
}
@Serializable @Serializable
data class SearchManga( data class SearchManga(
@ -47,25 +55,22 @@ 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,
) {
@Serializable
data class MangaDetailsData(
val manga: Manga, val manga: Manga,
) { )
@Serializable
data class Manga( @Serializable
data class Manga(
@SerialName("_id") val id: String, @SerialName("_id") val id: String,
val name: String, val name: String,
val thumbnail: String? = null, val thumbnail: String? = null,
@ -76,10 +81,9 @@ data class ApiMangaDetailsResponse(
val status: String? = null, val status: String? = null,
val altNames: List<String>? = emptyList(), val altNames: List<String>? = emptyList(),
val englishName: String? = null, val englishName: String? = null,
val nativeName: String? = null, ) {
) { fun toSManga() = SManga.create().apply {
fun toSManga(titleStyle: String?) = SManga.create().apply { title = englishName ?: name
title = titleStyle.preferedName(name, englishName, nativeName)
url = "/manga/$id/${name.titleToSlug()}" url = "/manga/$id/${name.titleToSlug()}"
thumbnail_url = thumbnail?.parseThumbnailUrl() thumbnail_url = thumbnail?.parseThumbnailUrl()
description = this@Manga.description?.parseDescription() description = this@Manga.description?.parseDescription()
@ -96,115 +100,56 @@ data class ApiMangaDetailsResponse(
author = authors.first().trim() author = authors.first().trim()
artist = author artist = author
} }
genre = "${genres?.joinToString { it.trim() }}, ${tags?.joinToString { it.trim() }}" genre = ((genres ?: emptyList()) + (tags ?: emptyList()))
.joinToString { it.trim() }
status = this@Manga.status.parseStatus() status = this@Manga.status.parseStatus()
} }
}
}
} }
// chapters details
@Serializable @Serializable
data class ApiChapterListResponse( data class ChapterListData(
val data: ChapterListData,
) {
@Serializable
data class ChapterListData(
val manga: ChapterList,
) {
@Serializable
data class ChapterList(
@SerialName("availableChaptersDetail") val chapters: AvailableChapters,
) {
@Serializable
data class AvailableChapters(
val sub: List<String>? = null,
)
}
}
}
@Serializable
data class ApiChapterListDetailsResponse(
val data: ChapterListData,
) {
@Serializable
data class ChapterListData(
@SerialName("episodeInfos") val chapterList: List<ChapterData>? = emptyList(), @SerialName("episodeInfos") val chapterList: List<ChapterData>? = emptyList(),
) { )
@Serializable
data class ChapterData( @Serializable
@SerialName("episodeIdNum") val chapterNum: Float, data class ChapterData(
@SerialName("episodeIdNum") val chapterNum: JsonPrimitive,
@SerialName("notes") val title: String? = null, @SerialName("notes") val title: String? = null,
val uploadDates: DateDto? = null, val uploadDates: DateDto? = null,
) { ) {
@Serializable fun toSChapter(mangaUrl: String) = SChapter.create().apply {
data class DateDto( name = "Chapter $chapterNum"
val sub: String? = null, 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 ApiPageListResponse( data class DateDto(
val data: PageListData, val sub: String? = null,
) { )
@Serializable
data class PageListData( // page lsit
@SerialName("chapterPages") val pageList: PageList?, @Serializable
) { data class PageListData(
@Serializable @SerialName("chapterPages") val pageList: Edges<Servers>?,
data class PageList( )
@SerialName("edges") val serverList: List<Servers>?,
) { @Serializable
@Serializable data class Servers(
data class Servers(
@SerialName("pictureUrlHead") val serverUrl: String? = null, @SerialName("pictureUrlHead") val serverUrl: String? = null,
val pictureUrls: List<PageUrl>?, val pictureUrls: List<PageUrl>?,
) { )
@Serializable
data class PageUrl( @Serializable
data class PageUrl(
val url: String, 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( @Serializable
data class PopularVariables(
val type: String, val type: String,
val size: Int, val size: Int,
val dateRange: Int, val dateRange: Int,
val page: Int, val page: Int,
val allowAdult: Boolean, val allowAdult: Boolean,
val allowUnknown: 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 SearchVariables(
val variables: ApiSearchVariables,
val query: String,
) {
@Serializable
data class ApiSearchVariables(
val search: SearchPayload, val search: SearchPayload,
val limit: Int, @SerialName("limit") val size: Int,
val page: Int, val page: Int,
val translationType: String, val translationType: String,
val countryOrigin: String, val countryOrigin: String,
) )
@Serializable @Serializable
data class SearchPayload( data class SearchPayload(
val query: String, val query: String?,
val sortBy: String?,
val genres: List<String>?, val genres: List<String>?,
val excludeGenres: List<String>?, val excludeGenres: List<String>?,
val isManga: Boolean, val isManga: Boolean,
val allowAdult: Boolean, val allowAdult: Boolean,
val allowUnknown: 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 IDVariables(
val variables: ApiIDVariables,
val query: String,
) {
@Serializable
data class ApiIDVariables(
val id: String, val id: String,
) )
constructor(
id: String,
graphqlQuery: String,
) : this(
ApiIDVariables(id),
graphqlQuery,
)
}
@Serializable @Serializable
data class ApiChapterListDetailsPayload( data class ChapterListVariables(
val variables: ApiChapterDetailsVariables,
val query: String,
) {
@Serializable
data class ApiChapterDetailsVariables(
val id: String, val id: String,
val chapterNumStart: Float, val chapterNumStart: Float,
val chapterNumEnd: Float, 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 PageListVariables(
val variables: ApiPageListVariables,
val query: String,
) {
@Serializable
data class ApiPageListVariables(
val id: String, val id: String,
val chapterNum: String, val chapterNum: String,
val translationType: String, val translationType: String,
) )
constructor(
id: String,
chapterNum: String,
translationType: String,
) : this(
ApiPageListVariables(
id = id,
chapterNum = chapterNum,
translationType = translationType,
),
PAGE_QUERY,
)
}

View File

@ -1,11 +1,8 @@
package eu.kanade.tachiyomi.extension.en.allanime 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(