Add AllAnime (#15854)
* Add AllAnime Thanks to Secozzi for the GraphQl queries Co-authored-by: Secozzi <49240133+Secozzi@users.noreply.github.com> * remove unnecessary comment * remove unnecessary `setOnPreferenceChangeListener` * unused variable * suggested changes Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> * unused import * [skip ci] whitespace * simplify page list fallback logic --------- Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
parent
2d368d9066
commit
100630a408
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name=".en.allanime.AllAnimeUrlActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:host="allanime.to"/>
|
||||||
|
<data android:host="allanime.co"/>
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:pathPattern="/manga/..*"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:pathPattern="/read/..*"
|
||||||
|
android:scheme="https" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
|
@ -0,0 +1,12 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'AllAnime'
|
||||||
|
pkgNameSuffix = 'en.allanime'
|
||||||
|
extClass = '.AllAnime'
|
||||||
|
extVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 165 KiB |
|
@ -0,0 +1,459 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.allanime
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class AllAnime : ConfigurableSource, HttpSource() {
|
||||||
|
|
||||||
|
override val name = "AllAnime"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences =
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
|
||||||
|
private val domain = preferences.getString(DOMAIN_PREF, "allanime.to")
|
||||||
|
|
||||||
|
override val baseUrl = "https://$domain"
|
||||||
|
|
||||||
|
private val apiUrl = "https://api.$domain/allanimeapi"
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimitHost(apiUrl.toHttpUrl(), 1)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
/* Popular */
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val showAdult = preferences.getBoolean(SHOW_ADULT_PREF, false)
|
||||||
|
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("type", "manga")
|
||||||
|
put("size", limit)
|
||||||
|
put("dateRange", 0)
|
||||||
|
put("page", page)
|
||||||
|
put("allowAdult", showAdult)
|
||||||
|
put("allowUnknown", false)
|
||||||
|
}
|
||||||
|
put("query", POPULAR_QUERY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.add("Content-Length", body.contentLength().toString())
|
||||||
|
.add("Content-Type", body.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val result = json.decodeFromString<ApiPopularResponse>(response.body.string())
|
||||||
|
|
||||||
|
val titleStyle = preferences.getString(TITLE_PREF, "romaji")!!
|
||||||
|
|
||||||
|
val mangaList = result.data.queryPopular.recommendations
|
||||||
|
.mapNotNull { it.anyCard }
|
||||||
|
.map { manga ->
|
||||||
|
SManga.create().apply {
|
||||||
|
title = when (titleStyle) {
|
||||||
|
"romaji" -> manga.name
|
||||||
|
"eng" -> manga.englishName ?: manga.name
|
||||||
|
else -> manga.nativeName ?: manga.name
|
||||||
|
}
|
||||||
|
url = "/manga/${manga._id}/${manga.name.titleToSlug()}"
|
||||||
|
thumbnail_url = manga.thumbnail.parseThumbnailUrl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangaList, mangaList.size == limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Latest */
|
||||||
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList())
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
/* Search */
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
if (!query.startsWith(SEARCH_PREFIX)) {
|
||||||
|
return super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "/manga/${query.substringAfter(SEARCH_PREFIX)}/"
|
||||||
|
return fetchMangaDetails(SManga.create().apply { this.url = url }).map {
|
||||||
|
MangasPage(listOf(it), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val showAdult = preferences.getBoolean(SHOW_ADULT_PREF, false)
|
||||||
|
var country = "ALL"
|
||||||
|
val includeGenres = mutableListOf<String>()
|
||||||
|
val excludeGenres = mutableListOf<String>()
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is GenreFilter -> {
|
||||||
|
filter.state.forEach { genreState ->
|
||||||
|
when (genreState.state) {
|
||||||
|
STATE_INCLUDE -> includeGenres.add(genreState.name)
|
||||||
|
STATE_EXCLUDE -> excludeGenres.add(genreState.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CountryFilter -> {
|
||||||
|
country = filter.getValue()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
putJsonObject("variables") {
|
||||||
|
putJsonObject("search") {
|
||||||
|
if (includeGenres.isNotEmpty() || excludeGenres.isNotEmpty()) {
|
||||||
|
put("genres", JsonArray(includeGenres.map { JsonPrimitive(it) }))
|
||||||
|
put("excludeGenres", JsonArray(excludeGenres.map { JsonPrimitive(it) }))
|
||||||
|
}
|
||||||
|
if (query.isNotEmpty()) put("query", query)
|
||||||
|
put("allowAdult", showAdult)
|
||||||
|
put("allowUnknown", false)
|
||||||
|
put("isManga", true)
|
||||||
|
}
|
||||||
|
put("limit", limit)
|
||||||
|
put("page", page)
|
||||||
|
put("translationType", "sub")
|
||||||
|
put("countryOrigin", country)
|
||||||
|
}
|
||||||
|
put("query", SEARCH_QUERY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.add("Content-Length", body.contentLength().toString())
|
||||||
|
.add("Content-Type", body.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val result = json.decodeFromString<ApiSearchResponse>(response.body.string())
|
||||||
|
|
||||||
|
val titleStyle = preferences.getString(TITLE_PREF, "romaji")!!
|
||||||
|
|
||||||
|
val mangaList = result.data.mangas.edges
|
||||||
|
.map { manga ->
|
||||||
|
SManga.create().apply {
|
||||||
|
title = when (titleStyle) {
|
||||||
|
"romaji" -> manga.name
|
||||||
|
"eng" -> manga.englishName ?: manga.name
|
||||||
|
else -> manga.nativeName ?: manga.name
|
||||||
|
}
|
||||||
|
url = "/manga/${manga._id}/${manga.name.titleToSlug()}"
|
||||||
|
thumbnail_url = manga.thumbnail.parseThumbnailUrl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangaList, mangaList.size == limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() = filters
|
||||||
|
|
||||||
|
/* Details */
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val mangaId = manga.url.split("/")[2]
|
||||||
|
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("_id", mangaId)
|
||||||
|
}
|
||||||
|
put("query", DETAILS_QUERY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.add("Content-Length", body.contentLength().toString())
|
||||||
|
.add("Content-Type", body.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val result = json.decodeFromString<ApiMangaDetailsResponse>(response.body.string())
|
||||||
|
val manga = result.data.manga
|
||||||
|
|
||||||
|
val titleStyle = preferences.getString(TITLE_PREF, "romaji")!!
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = when (titleStyle) {
|
||||||
|
"romaji" -> manga.name
|
||||||
|
"eng" -> manga.englishName ?: manga.name
|
||||||
|
else -> manga.nativeName ?: manga.name
|
||||||
|
}
|
||||||
|
url = "/manga/${manga._id}/${manga.name.titleToSlug()}"
|
||||||
|
thumbnail_url = manga.thumbnail.parseThumbnailUrl()
|
||||||
|
description = Jsoup.parse(
|
||||||
|
manga.description?.replace("<br>", "br2n") ?: "",
|
||||||
|
).text().replace("br2n", "\n")
|
||||||
|
description += if (manga.altNames != null) {
|
||||||
|
"\n\nAlternative Names: ${manga.altNames.joinToString { it.trim() }}"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
if (manga.authors?.isNotEmpty() == true) {
|
||||||
|
author = manga.authors.first().trim()
|
||||||
|
artist = author
|
||||||
|
}
|
||||||
|
genre = "${manga.genres?.joinToString { it.trim() }}, ${manga.tags?.joinToString { it.trim() }}"
|
||||||
|
status = manga.status.parseStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
return "$baseUrl${manga.url}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chapters */
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return client.newCall(chapterListRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
chapterListParse(response, manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val mangaId = manga.url.split("/")[2]
|
||||||
|
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("_id", mangaId)
|
||||||
|
}
|
||||||
|
put("query", CHAPTERS_QUERY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.add("Content-Length", body.contentLength().toString())
|
||||||
|
.add("Content-Type", body.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||||
|
val result = json.decodeFromString<ApiChapterListResponse>(response.body.string())
|
||||||
|
|
||||||
|
val chapters = result.data.manga.availableChaptersDetail.sub
|
||||||
|
|
||||||
|
val mangaUrl = manga.url.substringAfter("/manga/")
|
||||||
|
|
||||||
|
return chapters?.map { chapter ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = "Chapter $chapter"
|
||||||
|
url = "/read/$mangaUrl/chapter-$chapter-sub"
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter): String {
|
||||||
|
return "$baseUrl${chapter.url}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
||||||
|
val chapterUrl = chapter.url.split("/")
|
||||||
|
val mangaId = chapterUrl[2]
|
||||||
|
val chapterNo = chapterUrl[4].split("-")[1]
|
||||||
|
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("mangaId", mangaId)
|
||||||
|
put("translationType", "sub")
|
||||||
|
put("chapterString", chapterNo)
|
||||||
|
}
|
||||||
|
put("query", PAGE_QUERY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.add("Content-Length", body.contentLength().toString())
|
||||||
|
.add("Content-Type", body.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pageListParse(response: Response, chapter: SChapter): List<Page> {
|
||||||
|
val result = json.decodeFromString<ApiPageListResponse>(response.body.string())
|
||||||
|
val pages = result.data.chapterPages?.edges?.get(0) ?: return emptyList()
|
||||||
|
|
||||||
|
val imageDomain = if (!pages.pictureUrlHead.isNullOrEmpty()) {
|
||||||
|
pages.pictureUrlHead.let { server ->
|
||||||
|
if (server.matches(urlRegex)) {
|
||||||
|
server
|
||||||
|
} else {
|
||||||
|
"https://$server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// in rare cases, the api doesn't return server url
|
||||||
|
// for that, we try to parse the frontend html to get it
|
||||||
|
val chapterUrl = getChapterUrl(chapter)
|
||||||
|
val frontendRequest = GET(chapterUrl, headers)
|
||||||
|
val url = client.newCall(frontendRequest).execute().use { frontendResponse ->
|
||||||
|
val document = frontendResponse.asJsoup()
|
||||||
|
val script = document.select("script:containsData(window.__NUXT__)").firstOrNull()
|
||||||
|
imageUrlFromPageRegex.matchEntire(script.toString())
|
||||||
|
?.groupValues
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.replace("\\u002F", "/")
|
||||||
|
?.substringBeforeLast(pages.pictureUrls.first().toString(), "")
|
||||||
|
}
|
||||||
|
url?.takeIf { it.isNotEmpty() } ?: return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages.pictureUrls.mapIndexed { index, image ->
|
||||||
|
Page(
|
||||||
|
index = index,
|
||||||
|
imageUrl = "$imageDomain${image.url}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helpers */
|
||||||
|
private fun String.parseThumbnailUrl(): String {
|
||||||
|
return if (this.matches(urlRegex)) {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
"$image_cdn$this?w=250"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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(titleSpecialCharactersRegex, "-")
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = DOMAIN_PREF
|
||||||
|
title = "Preferred domain"
|
||||||
|
entries = arrayOf("allanime.to", "allanime.co")
|
||||||
|
entryValues = arrayOf("allanime.to", "allanime.co")
|
||||||
|
setDefaultValue("allanime.to")
|
||||||
|
summary = "Requires App Restart"
|
||||||
|
}.let { screen.addPreference(it) }
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = TITLE_PREF
|
||||||
|
title = "Preferred Title Style"
|
||||||
|
entries = arrayOf("Romaji", "English", "Native")
|
||||||
|
entryValues = arrayOf("romaji", "eng", "native")
|
||||||
|
setDefaultValue("romaji")
|
||||||
|
summary = "%s"
|
||||||
|
}.let { screen.addPreference(it) }
|
||||||
|
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = SHOW_ADULT_PREF
|
||||||
|
title = "Show Adult Content"
|
||||||
|
setDefaultValue(false)
|
||||||
|
}.let { screen.addPreference(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val limit = 26
|
||||||
|
const val SEARCH_PREFIX = "id:"
|
||||||
|
private const val image_cdn = "https://wp.youtube-anime.com/aln.youtube-anime.com/"
|
||||||
|
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
|
private val urlRegex = Regex("^https?://.*")
|
||||||
|
private val titleSpecialCharactersRegex = Regex("[^a-z\\d]+")
|
||||||
|
private val imageUrlFromPageRegex = Regex("selectedPicturesServer:\\[\\{.*?url:\"(.*?)\".*?\\}\\]")
|
||||||
|
|
||||||
|
private const val DOMAIN_PREF = "pref_domain"
|
||||||
|
private const val TITLE_PREF = "pref_title"
|
||||||
|
private const val SHOW_ADULT_PREF = "pref_adult"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.allanime
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiPopularResponse(
|
||||||
|
val data: PopularResultData,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class PopularResultData(
|
||||||
|
val queryPopular: QueryPopularData,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class QueryPopularData(
|
||||||
|
val recommendations: List<Recommendation>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Recommendation(
|
||||||
|
val anyCard: Card? = null,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Card(
|
||||||
|
val _id: String,
|
||||||
|
val name: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val englishName: String? = null,
|
||||||
|
val nativeName: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiSearchResponse(
|
||||||
|
val data: SearchResultData,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SearchResultData(
|
||||||
|
val mangas: SearchResultMangas,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SearchResultMangas(
|
||||||
|
val edges: List<SearchResultEdge>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SearchResultEdge(
|
||||||
|
val _id: String,
|
||||||
|
val name: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val englishName: String? = null,
|
||||||
|
val nativeName: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiMangaDetailsResponse(
|
||||||
|
val data: MangaDetailsData,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MangaDetailsData(
|
||||||
|
val manga: MangaDetails,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class MangaDetails(
|
||||||
|
val _id: String,
|
||||||
|
val name: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val description: String?,
|
||||||
|
val authors: List<String>?,
|
||||||
|
val genres: List<String>?,
|
||||||
|
val tags: List<String>?,
|
||||||
|
val status: String?,
|
||||||
|
val altNames: List<String>?,
|
||||||
|
val englishName: String? = null,
|
||||||
|
val nativeName: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiChapterListResponse(
|
||||||
|
val data: ChapterListData,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class ChapterListData(
|
||||||
|
val manga: ChapterList,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class ChapterList(
|
||||||
|
val availableChaptersDetail: AvailableChapters,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class AvailableChapters(
|
||||||
|
val sub: List<String>? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiPageListResponse(
|
||||||
|
val data: PageListData,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class PageListData(
|
||||||
|
val chapterPages: PageList?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class PageList(
|
||||||
|
val edges: List<Servers>?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Servers(
|
||||||
|
val pictureUrlHead: String? = null,
|
||||||
|
val pictureUrls: List<PageUrl>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class PageUrl(
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.allanime
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
|
internal class Genre(name: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
internal class CountryFilter(name: String, private val countries: List<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(name, countries.map { it.first }.toTypedArray()) {
|
||||||
|
fun getValue() = countries[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class GenreFilter(title: String, genres: List<Genre>) :
|
||||||
|
Filter.Group<Genre>(title, genres)
|
||||||
|
|
||||||
|
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(
|
||||||
|
Pair("All", "ALL"),
|
||||||
|
Pair("Japan", "JP"),
|
||||||
|
Pair("China", "CN"),
|
||||||
|
Pair("Korea", "KR"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val filters = FilterList(
|
||||||
|
CountryFilter("Countries", countryList),
|
||||||
|
GenreFilter("Genres", genreList),
|
||||||
|
)
|
|
@ -0,0 +1,122 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.allanime
|
||||||
|
|
||||||
|
fun buildQuery(queryAction: () -> String): String {
|
||||||
|
return queryAction()
|
||||||
|
.trimIndent()
|
||||||
|
.replace("%", "$")
|
||||||
|
}
|
||||||
|
|
||||||
|
val POPULAR_QUERY: String = buildQuery {
|
||||||
|
"""
|
||||||
|
query(
|
||||||
|
%type: VaildPopularTypeEnumType!
|
||||||
|
%size: Int!
|
||||||
|
%page: Int
|
||||||
|
%dateRange: Int
|
||||||
|
%allowAdult: Boolean
|
||||||
|
%allowUnknown: Boolean
|
||||||
|
) {
|
||||||
|
queryPopular(
|
||||||
|
type: %type
|
||||||
|
size: %size
|
||||||
|
dateRange: %dateRange
|
||||||
|
page: %page
|
||||||
|
allowAdult: %allowAdult
|
||||||
|
allowUnknown: %allowUnknown
|
||||||
|
) {
|
||||||
|
recommendations {
|
||||||
|
anyCard {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
thumbnail
|
||||||
|
englishName
|
||||||
|
nativeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
val SEARCH_QUERY: String = buildQuery {
|
||||||
|
"""
|
||||||
|
query(
|
||||||
|
%search: SearchInput
|
||||||
|
%limit: Int
|
||||||
|
%page: Int
|
||||||
|
%translationType: VaildTranslationTypeMangaEnumType
|
||||||
|
%countryOrigin: VaildCountryOriginEnumType
|
||||||
|
) {
|
||||||
|
mangas(
|
||||||
|
search: %search
|
||||||
|
limit: %limit
|
||||||
|
page: %page
|
||||||
|
translationType: %translationType
|
||||||
|
countryOrigin: %countryOrigin
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
thumbnail
|
||||||
|
englishName
|
||||||
|
nativeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
val DETAILS_QUERY: String = buildQuery {
|
||||||
|
"""
|
||||||
|
query (%_id: String!) {
|
||||||
|
manga(
|
||||||
|
_id: %_id
|
||||||
|
) {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
thumbnail
|
||||||
|
description
|
||||||
|
authors
|
||||||
|
genres
|
||||||
|
tags
|
||||||
|
status
|
||||||
|
altNames
|
||||||
|
englishName
|
||||||
|
nativeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
val CHAPTERS_QUERY: String = buildQuery {
|
||||||
|
"""
|
||||||
|
query (%_id: String!) {
|
||||||
|
manga(
|
||||||
|
_id: %_id
|
||||||
|
) {
|
||||||
|
availableChaptersDetail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
val PAGE_QUERY: String = buildQuery {
|
||||||
|
"""
|
||||||
|
query(
|
||||||
|
%mangaId: String!,
|
||||||
|
%translationType: VaildTranslationTypeMangaEnumType!,
|
||||||
|
%chapterString: String!
|
||||||
|
) {
|
||||||
|
chapterPages(
|
||||||
|
mangaId: %mangaId
|
||||||
|
translationType: %translationType
|
||||||
|
chapterString: %chapterString
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
pictureUrls
|
||||||
|
pictureUrlHead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.allanime
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class AllAnimeUrlActivity : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
|
val id = pathSegments[1]
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
|
putExtra("query", "${AllAnime.SEARCH_PREFIX}$id")
|
||||||
|
putExtra("filter", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.e("AllAnimeUrlActivity", e.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("AllAnimeUrlActivity", "could not parse uri from intent $intent")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue