fix(all/animeonsen): Fix anime serializer (#2388)

This commit is contained in:
Claudemirovsky
2023-10-16 12:01:47 -03:00
committed by GitHub
parent 87b10660e6
commit e07fe1e8d6
3 changed files with 108 additions and 122 deletions

View File

@ -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'
} }

View File

@ -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",

View File

@ -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
}
}