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:
parent
7b48737ba8
commit
92b8f4906f
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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,
|
||||||
}
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue