fix(all/animeonsen): Fix anime serializer (#2388)
This commit is contained in:
@ -1,12 +1,14 @@
|
|||||||
apply plugin: 'com.android.application'
|
plugins {
|
||||||
apply plugin: 'kotlin-android'
|
alias(libs.plugins.android.application)
|
||||||
apply plugin: 'kotlinx-serialization'
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeOnsen'
|
extName = 'AnimeOnsen'
|
||||||
pkgNameSuffix = 'all.animeonsen'
|
pkgNameSuffix = 'all.animeonsen'
|
||||||
extClass = '.AnimeOnsen'
|
extClass = '.AnimeOnsen'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
libVersion = '13'
|
libVersion = '13'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.all.animeonsen
|
package eu.kanade.tachiyomi.animeextension.all.animeonsen
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeDetails
|
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.AnimeListItem
|
||||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeListResponse
|
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.SearchResponse
|
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.SearchResponse
|
||||||
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.VideoData
|
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.VideoData
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
@ -21,16 +21,14 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.boolean
|
import kotlinx.serialization.json.boolean
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
|
||||||
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 uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.Exception
|
import kotlin.Exception
|
||||||
|
|
||||||
class AnimeOnsen : ConfigurableAnimeSource, AnimeHttpSource() {
|
class AnimeOnsen : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
@ -53,105 +51,43 @@ class AnimeOnsen : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json = Json {
|
private val json: Json by injectLazy()
|
||||||
ignoreUnknownKeys = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder() = Headers.Builder().add("user-agent", AO_USER_AGENT)
|
||||||
.add("user-agent", AO_USER_AGENT)
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
// The site doesn't have a popular anime tab, so we use the home page instead (latest anime).
|
// The site doesn't have a popular anime tab, so we use the home page instead (latest anime).
|
||||||
override fun popularAnimeRequest(page: Int): Request =
|
override fun popularAnimeRequest(page: Int) =
|
||||||
GET("$apiUrl/content/index?start=${(page - 1) * 20}&limit=20")
|
GET("$apiUrl/content/index?start=${(page - 1) * 20}&limit=20")
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
val responseJson = json.decodeFromString<AnimeListResponse>(response.body.string())
|
val responseJson = response.parseAs<AnimeListResponse>()
|
||||||
val animes = responseJson.content.map {
|
val animes = responseJson.content.map { it.toSAnime() }
|
||||||
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
|
val hasNextPage = responseJson.cursor.next.firstOrNull()?.jsonPrimitive?.boolean == true
|
||||||
return AnimesPage(animes, hasNextPage)
|
return AnimesPage(animes, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
// =============================== Latest ===============================
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun latestUpdatesRequest(page: Int) = throw Exception("not used")
|
||||||
val contentId = response.request.url.toString().substringBeforeLast("/episodes")
|
override fun latestUpdatesParse(response: Response) = throw Exception("not used")
|
||||||
.substringAfterLast("/")
|
|
||||||
val responseJson = json.decodeFromString<JsonObject>(response.body.string())
|
|
||||||
return responseJson.keys.map { epNum ->
|
|
||||||
SEpisode.create().apply {
|
|
||||||
url = "$contentId/video/$epNum"
|
|
||||||
episode_number = epNum.toFloat()
|
|
||||||
val episodeName =
|
|
||||||
responseJson[epNum]!!.jsonObject["contentTitle_episode_en"]!!.jsonPrimitive.content
|
|
||||||
name = "Episode $epNum: $episodeName"
|
|
||||||
}
|
|
||||||
}.sortedByDescending { it.episode_number }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListRequest(anime: SAnime): Request {
|
|
||||||
return GET("$apiUrl/content/${anime.url}/episodes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val videoData = json.decodeFromString<VideoData>(response.body.string())
|
|
||||||
val videoUrl = videoData.uri.stream
|
|
||||||
val subtitleLangs = videoData.metadata.subtitles
|
|
||||||
val headers = Headers.headersOf(
|
|
||||||
"referer",
|
|
||||||
baseUrl,
|
|
||||||
"user-agent",
|
|
||||||
AO_USER_AGENT,
|
|
||||||
)
|
|
||||||
val video = try {
|
|
||||||
val subtitles = videoData.uri.subtitles.keys.toList().sortSubs().map {
|
|
||||||
val lang = subtitleLangs[it]!!.jsonPrimitive.content
|
|
||||||
val url = videoData.uri.subtitles[it]!!.jsonPrimitive.content
|
|
||||||
Track(url, lang)
|
|
||||||
}
|
|
||||||
Video(videoUrl, "Default (720p)", videoUrl, headers = headers, subtitleTracks = subtitles)
|
|
||||||
} catch (e: Error) {
|
|
||||||
Video(videoUrl, "Default (720p)", videoUrl, headers = headers)
|
|
||||||
}
|
|
||||||
return listOf(video)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode) = GET("$apiUrl/content/${episode.url}")
|
|
||||||
|
|
||||||
override fun videoUrlParse(response: Response) = throw Exception("not used")
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
|
||||||
|
GET("$apiUrl/search/$query")
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
val searchResult = json.decodeFromString<SearchResponse>(response.body.string()).result
|
val searchResult = response.parseAs<SearchResponse>().result
|
||||||
val results = searchResult.map {
|
val results = searchResult.map { it.toSAnime() }
|
||||||
it.toSAnime()
|
|
||||||
}
|
|
||||||
return AnimesPage(results, false)
|
return AnimesPage(results, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$apiUrl/search/$query")
|
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
override fun animeDetailsParse(response: Response): SAnime {
|
|
||||||
val details = json.decodeFromString<AnimeDetails>(response.body.string())
|
|
||||||
val anime = SAnime.create().apply {
|
|
||||||
url = details.content_id
|
|
||||||
title = 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 = "https://api.animeonsen.xyz/v4/image/210x300/${details.content_id}"
|
|
||||||
}
|
|
||||||
return anime
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||||
return client.newCall(GET("$apiUrl/content/${anime.url}/extensive"))
|
return client.newCall(GET("$apiUrl/content/${anime.url}/extensive"))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
@ -160,22 +96,63 @@ class AnimeOnsen : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
override fun animeDetailsRequest(anime: SAnime) = GET("$baseUrl/details/${anime.url}")
|
||||||
return GET("$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!!
|
||||||
|
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}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// ============================== Episodes ==============================
|
||||||
override fun latestUpdatesRequest(page: Int) = throw Exception("not used")
|
override fun episodeListRequest(anime: SAnime) = GET("$apiUrl/content/${anime.url}/episodes")
|
||||||
override fun latestUpdatesParse(response: Response) = throw Exception("not used")
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val contentId = response.request.url.toString().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}"
|
||||||
|
}
|
||||||
|
}.sortedByDescending { it.episode_number }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
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]!!
|
||||||
|
Track(subUrl, language)
|
||||||
|
}
|
||||||
|
|
||||||
|
val video = Video(videoUrl, "Default (720p)", videoUrl, headers, subtitleTracks = subs)
|
||||||
|
return listOf(video)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListRequest(episode: SEpisode) = GET("$apiUrl/content/${episode.url}")
|
||||||
|
|
||||||
|
override fun videoUrlParse(response: Response) = throw Exception("not used")
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
// ============================== Settings ==============================
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
val subLangPref = ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_SUB_KEY
|
key = PREF_SUB_KEY
|
||||||
title = PREF_SUB_TITLE
|
title = PREF_SUB_TITLE
|
||||||
entries = PREF_SUB_ENTRIES
|
entries = PREF_SUB_ENTRIES
|
||||||
entryValues = PREF_SUB_VALUES
|
entryValues = PREF_SUB_VALUES
|
||||||
setDefaultValue("en-US")
|
setDefaultValue(PREF_SUB_DEFAULT)
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
@ -184,11 +161,13 @@ class AnimeOnsen : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
val entry = entryValues[index] as String
|
val entry = entryValues[index] as String
|
||||||
preferences.edit().putString(key, entry).commit()
|
preferences.edit().putString(key, entry).commit()
|
||||||
}
|
}
|
||||||
}
|
}.also(screen::addPreference)
|
||||||
screen.addPreference(subLangPref)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
return use { it.body.string() }.let(json::decodeFromString)
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseStatus(statusString: String?): Int {
|
private fun parseStatus(statusString: String?): Int {
|
||||||
return when (statusString?.trim()) {
|
return when (statusString?.trim()) {
|
||||||
@ -200,28 +179,22 @@ class AnimeOnsen : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
private fun AnimeListItem.toSAnime() = SAnime.create().apply {
|
private fun AnimeListItem.toSAnime() = SAnime.create().apply {
|
||||||
url = content_id
|
url = content_id
|
||||||
title = content_title ?: content_title_en!!
|
title = content_title ?: content_title_en!!
|
||||||
thumbnail_url = "https://api.animeonsen.xyz/v4/image/210x300/$content_id"
|
thumbnail_url = "$apiUrl/image/210x300/$content_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<String>.sortSubs(): List<String> {
|
private fun Map<String, String>.sortSubs(): List<Map.Entry<String, String>> {
|
||||||
val language = preferences.getString(PREF_SUB_KEY, "en-US")
|
val sub = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||||
val newList = mutableListOf<String>()
|
|
||||||
var preferred = 0
|
return entries.sortedWith(
|
||||||
for (key in this) {
|
compareBy { it.key.contains(sub) },
|
||||||
if (key == language) {
|
).reversed()
|
||||||
newList.add(preferred, key)
|
|
||||||
preferred++
|
|
||||||
} else {
|
|
||||||
newList.add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val AO_USER_AGENT = "Aniyomi/app (mobile)"
|
const val AO_USER_AGENT = "Aniyomi/app (mobile)"
|
||||||
private const val PREF_SUB_KEY = "preferred_subLang"
|
private const val PREF_SUB_KEY = "preferred_subLang"
|
||||||
private const val PREF_SUB_TITLE = "Preferred sub language"
|
private const val PREF_SUB_TITLE = "Preferred sub language"
|
||||||
|
const val PREF_SUB_DEFAULT = "en-US"
|
||||||
private val PREF_SUB_ENTRIES = arrayOf(
|
private val PREF_SUB_ENTRIES = arrayOf(
|
||||||
"العربية", "Deutsch", "English", "Español (Spain)",
|
"العربية", "Deutsch", "English", "Español (Spain)",
|
||||||
"Español (Latin)", "Français", "Italiano",
|
"Español (Latin)", "Français", "Italiano",
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.all.animeonsen.dto
|
package eu.kanade.tachiyomi.animeextension.all.animeonsen.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.JsonTransformingSerializer
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AnimeListResponse(
|
data class AnimeListResponse(
|
||||||
@ -18,18 +22,23 @@ data class AnimeListItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AnimeListCursor(
|
data class AnimeListCursor(val next: JsonArray)
|
||||||
val next: JsonArray,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AnimeDetails(
|
data class AnimeDetails(
|
||||||
val content_id: String,
|
val content_id: String,
|
||||||
val content_title: String?,
|
val content_title: String?,
|
||||||
val content_title_en: String?,
|
val content_title_en: String?,
|
||||||
|
@Serializable(with = MalSerializer::class)
|
||||||
val mal_data: MalData?,
|
val mal_data: MalData?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EpisodeDto(
|
||||||
|
@SerialName("contentTitle_episode_en")
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MalData(
|
data class MalData(
|
||||||
val genres: List<Genre>?,
|
val genres: List<Genre>?,
|
||||||
@ -39,14 +48,10 @@ data class MalData(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Genre(
|
data class Genre(val name: String)
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Studio(
|
data class Studio(val name: String)
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class VideoData(
|
data class VideoData(
|
||||||
@ -55,14 +60,12 @@ data class VideoData(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MetaData(
|
data class MetaData(val subtitles: Map<String, String>)
|
||||||
val subtitles: JsonObject,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StreamData(
|
data class StreamData(
|
||||||
val stream: String,
|
val stream: String,
|
||||||
val subtitles: JsonObject,
|
val subtitles: Map<String, String>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -70,3 +73,11 @@ data class SearchResponse(
|
|||||||
val status: Int,
|
val status: Int,
|
||||||
val result: List<AnimeListItem>,
|
val result: List<AnimeListItem>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object MalSerializer : JsonTransformingSerializer<MalData>(MalData.serializer()) {
|
||||||
|
override fun transformDeserialize(element: JsonElement): JsonElement =
|
||||||
|
when (element) {
|
||||||
|
is JsonPrimitive -> JsonObject(emptyMap())
|
||||||
|
else -> element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user