mirror of
https://github.com/yuzono/anime-extensions.git
synced 2026-06-13 13:39:44 +00:00
AnimeOnsen [Multi]: Fix Error 401, Add Filters & Title Language Preference (#402)
* Fix auth, add genres, language preference * Reformat the genres * Update title preference Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update interceptor * Add new animedetails info * Fix title entry's name * Update * Update 2 * Hotfix * refactor: improve error handling and optimize response parsing --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'AnimeOnsen'
|
||||
extClass = '.AnimeOnsen'
|
||||
extVersionCode = 9
|
||||
extVersionCode = 10
|
||||
}
|
||||
|
||||
apply plugin: "kei.plugins.extension.legacy"
|
||||
|
||||
@@ -1,55 +1,114 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.animeonsen
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.AnimeOnsen.Companion.AO_USER_AGENT
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import keiyoushi.utils.bodyString
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.toJsonRequestBody
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
|
||||
class AOAPIInterceptor(client: OkHttpClient, apiUrl: String) : Interceptor {
|
||||
class AOAPIInterceptor(private val client: OkHttpClient, apiUrl: String) : Interceptor {
|
||||
|
||||
private val token: String by lazy {
|
||||
runCatching {
|
||||
val body = """
|
||||
{
|
||||
"client_id": "f296be26-28b5-4358-b5a1-6259575e23b7",
|
||||
"client_secret": "349038c4157d0480784753841217270c3c5b35f4281eaee029de21cb04084235",
|
||||
"grant_type": "client_credentials"
|
||||
}
|
||||
""".trimIndent().toJsonRequestBody()
|
||||
private var token: String? = null
|
||||
|
||||
val headers = Headers.headersOf("user-agent", AO_USER_AGENT)
|
||||
private val host: String = apiUrl.toHttpUrlOrNull()?.host ?: apiUrl
|
||||
|
||||
val tokenObject = client.newCall(
|
||||
// Create a separate client for fetching the token to avoid infinite recursion
|
||||
private val tokenClient by lazy {
|
||||
client.newBuilder()
|
||||
.apply { interceptors().removeAll { it is AOAPIInterceptor } }
|
||||
.build()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun fetchToken(): String? {
|
||||
return try {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("client_id", "f296be26-28b5-4358-b5a1-6259575e23b7")
|
||||
.add("client_secret", "349038c4157d0480784753841217270c3c5b35f4281eaee029de21cb04084235")
|
||||
.add("grant_type", "client_credentials")
|
||||
.build()
|
||||
|
||||
val headers = Headers.headersOf(
|
||||
"User-Agent",
|
||||
AO_USER_AGENT,
|
||||
"Accept",
|
||||
"application/json",
|
||||
"Origin",
|
||||
"https://www.animeonsen.xyz",
|
||||
"Referer",
|
||||
"https://www.animeonsen.xyz/",
|
||||
)
|
||||
|
||||
val response = tokenClient.newCall(
|
||||
POST(
|
||||
"https://auth.animeonsen.xyz/oauth/token",
|
||||
headers,
|
||||
body,
|
||||
formBody,
|
||||
),
|
||||
).execute().parseAs<JsonObject>()
|
||||
).execute()
|
||||
|
||||
tokenObject["access_token"]!!.jsonPrimitive.content
|
||||
}.getOrElse { "" }
|
||||
val responseBody = response.bodyString()
|
||||
|
||||
// If we still get an HTML page (Cloudflare block or wrong endpoint), fail gracefully
|
||||
if (responseBody.isBlank() || responseBody.trimStart().startsWith("<")) {
|
||||
return null
|
||||
}
|
||||
|
||||
val tokenObject = responseBody.parseAs<JsonObject>()
|
||||
tokenObject["access_token"]?.jsonPrimitive?.content
|
||||
} catch (_: Throwable) {
|
||||
// Silently fail so we don't break endpoints that don't require auth (like Search)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val host: String = apiUrl.toHttpUrlOrNull()?.host ?: apiUrl
|
||||
@Synchronized
|
||||
private fun getOrRefreshToken(oldToken: String?): String? {
|
||||
if (token == oldToken) {
|
||||
token = fetchToken()
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (originalRequest.url.host == host) {
|
||||
val newRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer $token")
|
||||
.build()
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
// Only apply API token to the API host, let SearchInterceptor handle the search host
|
||||
if (originalRequest.url.host != host) {
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
|
||||
return chain.proceed(originalRequest)
|
||||
val currentToken = getOrRefreshToken(null)
|
||||
|
||||
val request = if (currentToken != null) {
|
||||
originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer $currentToken")
|
||||
.build()
|
||||
} else {
|
||||
originalRequest
|
||||
}
|
||||
|
||||
val response = chain.proceed(request)
|
||||
|
||||
if (response.code == 401) {
|
||||
response.close()
|
||||
val newToken = getOrRefreshToken(currentToken)
|
||||
|
||||
if (newToken != null) {
|
||||
val newRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer $newToken")
|
||||
.build()
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeDetails
|
||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeListItem
|
||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeListResponse
|
||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.EpisodeDto
|
||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.MeilisearchResponse
|
||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.SearchResponse
|
||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.VideoData
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
@@ -18,6 +19,7 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import keiyoushi.utils.firstInstanceOrNull
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.toJsonRequestBody
|
||||
@@ -28,6 +30,7 @@ import kotlinx.serialization.json.put
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AnimeOnsen :
|
||||
AnimeHttpSource(),
|
||||
@@ -54,17 +57,25 @@ class AnimeOnsen :
|
||||
|
||||
private val preferences by getPreferencesLazy()
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().add("user-agent", AO_USER_AGENT)
|
||||
override fun headersBuilder() = Headers.Builder()
|
||||
.add("User-Agent", AO_USER_AGENT)
|
||||
.add("Accept", "application/json, text/plain, */*")
|
||||
.add("Accept-Language", "en-US,en;q=0.9")
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("Origin", baseUrl)
|
||||
.add("Sec-Fetch-Dest", "empty")
|
||||
.add("Sec-Fetch-Mode", "cors")
|
||||
.add("Sec-Fetch-Site", "same-site")
|
||||
|
||||
private val preferredTitle: String
|
||||
get() = preferences.getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
|
||||
|
||||
// ============================== Popular ===============================
|
||||
// The site doesn't have a popular anime tab, so we use the home page instead (latest anime).
|
||||
override fun popularAnimeRequest(page: Int) = GET("$apiUrl/content/index?start=${(page - 1) * 20}&limit=20")
|
||||
override fun popularAnimeRequest(page: Int) = GET("$apiUrl/content/index?start=${(page - 1) * 30}&limit=30", headers)
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val responseJson = response.parseAs<AnimeListResponse>()
|
||||
val animes = responseJson.content.map { it.toSAnime() }
|
||||
// we can't (easily) serialize this thing because it returns a array with
|
||||
// two types: a boolean and a integer.
|
||||
val hasNextPage = responseJson.cursor.next.firstOrNull()?.jsonPrimitive?.boolean == true
|
||||
return AnimesPage(animes, hasNextPage)
|
||||
}
|
||||
@@ -74,48 +85,125 @@ class AnimeOnsen :
|
||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val postBody = buildJsonObject {
|
||||
put("q", query)
|
||||
}.toJsonRequestBody()
|
||||
override fun getFilterList(): AnimeFilterList = AnimeOnsenFilters.FILTER_LIST
|
||||
|
||||
return POST("$searchUrl/indexes/content/search", body = postBody)
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
if (query.isNotBlank()) {
|
||||
val postBody = buildJsonObject {
|
||||
put("q", query)
|
||||
}.toJsonRequestBody()
|
||||
|
||||
return POST("$searchUrl/indexes/content/search", headers, postBody)
|
||||
}
|
||||
|
||||
val genre = filters.firstInstanceOrNull<AnimeOnsenFilters.GenreFilter>()?.getValue()
|
||||
|
||||
return if (!genre.isNullOrBlank()) {
|
||||
GET("$apiUrl/content/index/genre/$genre", headers)
|
||||
} else {
|
||||
val start = (page - 1) * 30
|
||||
GET("$apiUrl/content/index?start=$start&limit=30", headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val searchResult = response.parseAs<SearchResponse>().hits
|
||||
val results = searchResult.map { it.toSAnime() }
|
||||
return AnimesPage(results, false)
|
||||
val requestUrl = response.request.url.toString()
|
||||
|
||||
return if (requestUrl.contains("indexes/content/search")) {
|
||||
val searchResult = response.parseAs<MeilisearchResponse>().hits
|
||||
val results = searchResult.map { it.toSAnime() }
|
||||
AnimesPage(results, false)
|
||||
} else if (requestUrl.contains("/genre/")) {
|
||||
val searchResult = response.parseAs<SearchResponse>().result
|
||||
val results = searchResult.map { it.toSAnime() }
|
||||
AnimesPage(results, false)
|
||||
} else {
|
||||
val responseJson = response.parseAs<AnimeListResponse>()
|
||||
val animes = responseJson.content.map { it.toSAnime() }
|
||||
val hasNextPage = responseJson.cursor.next.firstOrNull()?.jsonPrimitive?.boolean == true
|
||||
AnimesPage(animes, hasNextPage)
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsRequest(anime: SAnime) = GET("$apiUrl/content/${anime.url}/extensive")
|
||||
override fun animeDetailsRequest(anime: SAnime) = GET("$apiUrl/content/${anime.url}/extensive", headers)
|
||||
|
||||
override fun getAnimeUrl(anime: SAnime) = "$baseUrl/details/${anime.url}"
|
||||
|
||||
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
|
||||
val details = response.parseAs<AnimeDetails>()
|
||||
url = details.content_id
|
||||
title = details.content_title ?: details.content_title_en!!
|
||||
title = when (preferredTitle) {
|
||||
"english" -> details.content_title_en ?: details.content_title!!
|
||||
else -> details.content_title ?: details.content_title_en!!
|
||||
}
|
||||
status = parseStatus(details.mal_data?.status)
|
||||
author = details.mal_data?.studios?.joinToString { it.name }
|
||||
genre = details.mal_data?.genres?.joinToString { it.name }
|
||||
description = details.mal_data?.synopsis
|
||||
thumbnail_url = "$apiUrl/image/210x300/${details.content_id}"
|
||||
|
||||
val descBuilder = StringBuilder()
|
||||
|
||||
details.mal_data?.mean_score?.let { score ->
|
||||
val starCount = (score / 2.0).roundToInt().coerceIn(0, 5)
|
||||
val stars = "★".repeat(starCount) + "☆".repeat(5 - starCount)
|
||||
descBuilder.append("$stars $score\n\n")
|
||||
}
|
||||
|
||||
// Main synopsis
|
||||
details.mal_data?.synopsis?.let { descBuilder.append(it) }
|
||||
|
||||
val subsList = try {
|
||||
val epsResponse = client.newCall(GET("$apiUrl/content/${details.content_id}/episodes", headers)).execute()
|
||||
val epsJson = epsResponse.parseAs<Map<String, EpisodeDto>>()
|
||||
|
||||
val firstEpNum = epsJson.keys.firstOrNull()
|
||||
if (firstEpNum != null) {
|
||||
val videoResponse = client.newCall(GET("$apiUrl/content/${details.content_id}/video/$firstEpNum", headers)).execute()
|
||||
val videoData = videoResponse.parseAs<VideoData>()
|
||||
videoData.metadata.subtitles.values
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val extras = buildList {
|
||||
details.mal_data?.rating?.let {
|
||||
val formattedRating = it.replace("_", " ").uppercase()
|
||||
add("**Rating:** $formattedRating")
|
||||
}
|
||||
if (subsList.isNotEmpty()) {
|
||||
add("**Subtitles:** ${subsList.joinToString(", ")}")
|
||||
}
|
||||
details.mal_id?.let { add("[MAL](https://myanimelist.net/anime/$it)") }
|
||||
}
|
||||
|
||||
if (extras.isNotEmpty()) {
|
||||
descBuilder.append("\n\n")
|
||||
descBuilder.append(extras.joinToString("\n"))
|
||||
}
|
||||
|
||||
description = descBuilder.toString().trimEnd()
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListRequest(anime: SAnime) = GET("$apiUrl/content/${anime.url}/episodes")
|
||||
override fun episodeListRequest(anime: SAnime) = GET("$apiUrl/content/${anime.url}/episodes", headers)
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val contentId = response.request.url.toString().substringBeforeLast("/episodes")
|
||||
val contentId = response.request.url.toString().removeSuffix("/")
|
||||
.substringBeforeLast("/episodes")
|
||||
.substringAfterLast("/")
|
||||
val responseJson = response.parseAs<Map<String, EpisodeDto>>()
|
||||
return responseJson.map { (epNum, item) ->
|
||||
SEpisode.create().apply {
|
||||
url = "$contentId/video/$epNum"
|
||||
episode_number = epNum.toFloat()
|
||||
name = "Episode $epNum: ${item.name}"
|
||||
episode_number = epNum.toFloatOrNull() ?: -1f
|
||||
name = when (preferredTitle) {
|
||||
"english" -> "Episode $epNum: ${item.nameEn ?: item.nameJp ?: ""}"
|
||||
else -> "Episode $epNum: ${item.nameJp ?: item.nameEn ?: ""}"
|
||||
}
|
||||
}
|
||||
}.sortedByDescending { it.episode_number }
|
||||
}
|
||||
@@ -125,10 +213,9 @@ class AnimeOnsen :
|
||||
val videoData = response.parseAs<VideoData>()
|
||||
val videoUrl = videoData.uri.stream
|
||||
val subtitleLangs = videoData.metadata.subtitles
|
||||
val headers = headersBuilder().add("referer", baseUrl).build()
|
||||
|
||||
val subs = videoData.uri.subtitles.sortSubs().map { (langPrefix, subUrl) ->
|
||||
val language = subtitleLangs[langPrefix]!!
|
||||
val subs = videoData.uri.subtitles.sortSubs().mapNotNull { (langPrefix, subUrl) ->
|
||||
val language = subtitleLangs[langPrefix] ?: return@mapNotNull null
|
||||
Track(subUrl, language)
|
||||
}
|
||||
|
||||
@@ -136,12 +223,21 @@ class AnimeOnsen :
|
||||
return listOf(video)
|
||||
}
|
||||
|
||||
override fun videoListRequest(episode: SEpisode) = GET("$apiUrl/content/${episode.url}")
|
||||
override fun videoListRequest(episode: SEpisode) = GET("$apiUrl/content/${episode.url}", headers)
|
||||
|
||||
override fun videoUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_TITLE_KEY
|
||||
title = PREF_TITLE_TITLE
|
||||
entries = PREF_TITLE_ENTRIES
|
||||
entryValues = PREF_TITLE_VALUES
|
||||
setDefaultValue(PREF_TITLE_DEFAULT)
|
||||
summary = "%s"
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SUB_KEY
|
||||
title = PREF_SUB_TITLE
|
||||
@@ -160,8 +256,12 @@ class AnimeOnsen :
|
||||
|
||||
private fun AnimeListItem.toSAnime() = SAnime.create().apply {
|
||||
url = content_id
|
||||
title = content_title ?: content_title_en!!
|
||||
thumbnail_url = "$apiUrl/image/210x300/$content_id"
|
||||
title = when (preferredTitle) {
|
||||
"english" -> content_title_en ?: content_title ?: content_title_jp!!
|
||||
else -> content_title ?: content_title_jp ?: content_title_en!!
|
||||
}
|
||||
// Reference way: dynamically construct the thumbnail URL from the ID if not provided directly by search
|
||||
thumbnail_url = thumbnail ?: content_image ?: "$apiUrl/image/210x300/$content_id"
|
||||
}
|
||||
|
||||
private fun Map<String, String>.sortSubs(): List<Map.Entry<String, String>> {
|
||||
@@ -171,19 +271,30 @@ class AnimeOnsen :
|
||||
compareBy { it.key.contains(sub) },
|
||||
).reversed()
|
||||
}
|
||||
}
|
||||
|
||||
const val AO_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.3"
|
||||
private const val PREF_SUB_KEY = "preferred_subLang"
|
||||
private const val PREF_SUB_TITLE = "Preferred sub language"
|
||||
const val PREF_SUB_DEFAULT = "en-US"
|
||||
private val PREF_SUB_ENTRIES = arrayOf(
|
||||
"العربية", "Deutsch", "English", "Español (Spain)",
|
||||
"Español (Latin)", "Français", "Italiano",
|
||||
"Português (Brasil)", "Русский",
|
||||
)
|
||||
private val PREF_SUB_VALUES = arrayOf(
|
||||
"ar-ME", "de-DE", "en-US", "es-ES",
|
||||
"es-LA", "fr-FR", "it-IT",
|
||||
"pt-BR", "ru-RU",
|
||||
)
|
||||
companion object {
|
||||
const val AO_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.3"
|
||||
|
||||
// Title and episode name preferences
|
||||
private const val PREF_TITLE_KEY = "preferred_title"
|
||||
private const val PREF_TITLE_TITLE = "Preferred Title Language"
|
||||
private const val PREF_TITLE_DEFAULT = "romaji"
|
||||
private val PREF_TITLE_ENTRIES = arrayOf("Romaji", "English")
|
||||
private val PREF_TITLE_VALUES = arrayOf("romaji", "english")
|
||||
|
||||
// Subtitle preferences
|
||||
private const val PREF_SUB_KEY = "preferred_subLang"
|
||||
private const val PREF_SUB_TITLE = "Preferred sub language"
|
||||
const val PREF_SUB_DEFAULT = "en-US"
|
||||
private val PREF_SUB_ENTRIES = arrayOf(
|
||||
"العربية", "Deutsch", "English", "Español (Spain)",
|
||||
"Español (Latin)", "Français", "Italiano",
|
||||
"Português (Brasil)", "Русский",
|
||||
)
|
||||
private val PREF_SUB_VALUES = arrayOf(
|
||||
"ar-ME", "de-DE", "en-US", "es-ES",
|
||||
"es-LA", "fr-FR", "it-IT",
|
||||
"pt-BR", "ru-RU",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.animeonsen
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object AnimeOnsenFilters {
|
||||
class GenreFilter : AnimeFilter.Select<String>("Genre", GENRE_ENTRIES.toTypedArray(), 0) {
|
||||
fun getValue() = if (state == 0) null else GENRE_VALUES[state]
|
||||
|
||||
companion object {
|
||||
private val GENRE_ENTRIES = listOf(
|
||||
"All",
|
||||
"Action",
|
||||
"Adult Cast",
|
||||
"Adventure",
|
||||
"Anthropomorphic",
|
||||
"Avant Garde",
|
||||
"Award Winning",
|
||||
"Childcare",
|
||||
"Combat Sports",
|
||||
"Comedy",
|
||||
"Crossdressing",
|
||||
"Cute Girls Doing Cute Things",
|
||||
"Delinquents",
|
||||
"Detective",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Educational",
|
||||
"Fantasy",
|
||||
"Gag Humor",
|
||||
"Gore",
|
||||
"Gourmet",
|
||||
"Harem",
|
||||
"High Stakes Game",
|
||||
"Historical",
|
||||
"Horror",
|
||||
"Idols (Female)",
|
||||
"Idols (Male)",
|
||||
"Isekai",
|
||||
"Iyashikei",
|
||||
"Love Polygon",
|
||||
"Josei",
|
||||
"Kids",
|
||||
"Magical Sex Shift",
|
||||
"Mahou Shoujo",
|
||||
"Martial Arts",
|
||||
"Mecha",
|
||||
"Medical",
|
||||
"Military",
|
||||
"Music",
|
||||
"Mystery",
|
||||
"Mythology",
|
||||
"Organized Crime",
|
||||
"Otaku Culture",
|
||||
"Parody",
|
||||
"Performing Arts",
|
||||
"Pets",
|
||||
"Psychological",
|
||||
"Racing",
|
||||
"Reincarnation",
|
||||
"Reverse Harem",
|
||||
"Romance",
|
||||
"Romantic Subtext",
|
||||
"Samurai",
|
||||
"School",
|
||||
"Sci-Fi",
|
||||
"Seinen",
|
||||
"Shoujo",
|
||||
"Shoujo Ai",
|
||||
"Shounen",
|
||||
"Shounen Ai",
|
||||
"Showbiz",
|
||||
"Slice of Life",
|
||||
"Space", "Sports",
|
||||
"Strategy Game",
|
||||
"Super Power",
|
||||
"Supernatural",
|
||||
"Survival",
|
||||
"Team Sports",
|
||||
"Suspense",
|
||||
"Time Travel",
|
||||
"Vampire",
|
||||
"Video Game",
|
||||
"Visual Arts",
|
||||
"Workplace",
|
||||
)
|
||||
private val GENRE_VALUES = listOf(
|
||||
"",
|
||||
"action",
|
||||
"adult-cast",
|
||||
"adventure",
|
||||
"anthropomorphic",
|
||||
"avant-garde",
|
||||
"award-winning",
|
||||
"childcare",
|
||||
"combat-sports",
|
||||
"comedy",
|
||||
"crossdressing",
|
||||
"cgdct",
|
||||
"delinquents",
|
||||
"detective",
|
||||
"drama",
|
||||
"ecchi",
|
||||
"educational",
|
||||
"fantasy",
|
||||
"gag-humor",
|
||||
"gore",
|
||||
"gourmet",
|
||||
"harem",
|
||||
"high-stakes-game",
|
||||
"historical",
|
||||
"horror",
|
||||
"idols-female",
|
||||
"idols-male",
|
||||
"isekai",
|
||||
"iyashikei",
|
||||
"love-polygon",
|
||||
"josei",
|
||||
"kids",
|
||||
"magical-sex-shift",
|
||||
"mahou-shoujo",
|
||||
"martial-arts",
|
||||
"mecha",
|
||||
"medical",
|
||||
"military",
|
||||
"music",
|
||||
"mystery",
|
||||
"mythology",
|
||||
"organized-crime",
|
||||
"otaku-culture",
|
||||
"parody",
|
||||
"performing-arts",
|
||||
"pets",
|
||||
"psychological",
|
||||
"racing",
|
||||
"reincarnation",
|
||||
"reverse-harem",
|
||||
"romance",
|
||||
"romantic-subtext",
|
||||
"samurai",
|
||||
"school",
|
||||
"sci-fi",
|
||||
"seinen",
|
||||
"shoujo",
|
||||
"shoujo-ai",
|
||||
"shounen",
|
||||
"shounen-ai",
|
||||
"showbiz",
|
||||
"slice-of-life",
|
||||
"space",
|
||||
"sports",
|
||||
"strategy-game",
|
||||
"super-power",
|
||||
"supernatural",
|
||||
"survival",
|
||||
"team-sports",
|
||||
"suspense",
|
||||
"time-travel",
|
||||
"vampire",
|
||||
"video-game",
|
||||
"visual-arts",
|
||||
"workplace",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val FILTER_LIST get() = AnimeFilterList(
|
||||
GenreFilter(),
|
||||
)
|
||||
}
|
||||
@@ -12,9 +12,7 @@ class SearchInterceptor(client: OkHttpClient, baseUrl: String, searchUrl: String
|
||||
private val token: String by lazy {
|
||||
runCatching {
|
||||
val document = client.newCall(
|
||||
GET(
|
||||
baseUrl,
|
||||
),
|
||||
GET(baseUrl),
|
||||
).execute().useAsJsoup()
|
||||
|
||||
document.selectFirst("meta[name=ao-search-token]")?.attr("content") ?: ""
|
||||
|
||||
@@ -19,6 +19,9 @@ data class AnimeListItem(
|
||||
val content_id: String,
|
||||
val content_title: String? = null,
|
||||
val content_title_en: String? = null,
|
||||
val content_title_jp: String? = null,
|
||||
val content_image: String? = null,
|
||||
val thumbnail: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -29,6 +32,8 @@ data class AnimeDetails(
|
||||
val content_id: String,
|
||||
val content_title: String?,
|
||||
val content_title_en: String?,
|
||||
val mal_id: Int? = null,
|
||||
val subtitle_support: Boolean? = null,
|
||||
@Serializable(with = MalSerializer::class)
|
||||
val mal_data: MalData?,
|
||||
)
|
||||
@@ -36,7 +41,9 @@ data class AnimeDetails(
|
||||
@Serializable
|
||||
data class EpisodeDto(
|
||||
@SerialName("contentTitle_episode_en")
|
||||
val name: String,
|
||||
val nameEn: String? = null,
|
||||
@SerialName("contentTitle_episode_jp")
|
||||
val nameJp: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -45,6 +52,8 @@ data class MalData(
|
||||
val status: String?,
|
||||
val studios: List<Studio>?,
|
||||
val synopsis: String?,
|
||||
val mean_score: Double? = null,
|
||||
val rating: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -69,10 +78,16 @@ data class StreamData(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchResponse(
|
||||
data class MeilisearchResponse(
|
||||
val hits: List<AnimeListItem>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchResponse(
|
||||
val status: Int,
|
||||
val result: List<AnimeListItem>,
|
||||
)
|
||||
|
||||
object MalSerializer : JsonTransformingSerializer<MalData>(MalData.serializer()) {
|
||||
override fun transformDeserialize(element: JsonElement): JsonElement = when (element) {
|
||||
is JsonPrimitive -> JsonObject(emptyMap())
|
||||
|
||||
Reference in New Issue
Block a user