fix(all/sudatchi): Replace api calls with NEXT json (#3326)

This commit is contained in:
hollow 2024-06-10 20:38:34 +00:00 committed by GitHub
parent ee69716a7a
commit 356ce6f2a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 60 deletions

View File

@ -1,7 +1,8 @@
ext { ext {
extName = 'Sudatchi' extName = 'Sudatchi'
extClass = '.Sudatchi' extClass = '.Sudatchi'
extVersionCode = 2 extVersionCode = 3
isNsfw = true
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -4,12 +4,13 @@ import android.app.Application
import android.content.SharedPreferences 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.sudatchi.dto.AnimeDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.AnimePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.DirectoryDto import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.DirectoryDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomeListDto import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.EpisodePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.LongAnimeDto import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.ShortAnimeDto import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.PropsDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.SubtitleDto import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.SubtitleDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.WatchDto
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -27,6 +28,7 @@ import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document
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 uy.kohesive.injekt.injectLazy
@ -37,6 +39,8 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override val baseUrl = "https://sudatchi.com" override val baseUrl = "https://sudatchi.com"
private val ipfsUrl = "https://ipfs.animeui.com"
override val lang = "all" override val lang = "all"
override val supportsLatest = true override val supportsLatest = true
@ -52,7 +56,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
} }
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/api/home-list", headers) override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
private fun Int.parseStatus() = when (this) { private fun Int.parseStatus() = when (this) {
1 -> SAnime.UNKNOWN // Not Yet Released 1 -> SAnime.UNKNOWN // Not Yet Released
@ -61,7 +65,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
else -> SAnime.UNKNOWN else -> SAnime.UNKNOWN
} }
private fun ShortAnimeDto.toSAnime(titleLang: String) = SAnime.create().apply { private fun AnimeDto.toSAnime(titleLang: String) = SAnime.create().apply {
url = "/anime/$slug" url = "/anime/$slug"
title = when (titleLang) { title = when (titleLang) {
"romaji" -> titleRomanji "romaji" -> titleRomanji
@ -70,14 +74,19 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
} ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it } } ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it }
description = synopsis description = synopsis
status = statusId.parseStatus() status = statusId.parseStatus()
thumbnail_url = "$baseUrl$imgUrl" thumbnail_url = when {
imgUrl.startsWith("/") -> "$baseUrl$imgUrl"
else -> "$ipfsUrl/ipfs/$imgUrl"
}
genre = animeGenres?.joinToString { it.genre.name } genre = animeGenres?.joinToString { it.genre.name }
} }
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
sudatchiFilters.fetchFilters() sudatchiFilters.fetchFilters()
val titleLang = preferences.title val titleLang = preferences.title
return AnimesPage(response.parseAs<HomeListDto>().animeSpotlight.map { it.toSAnime(titleLang) }, false) val document = response.asJsoup()
val data = document.parseAs<HomePageDto>().animeSpotlight
return AnimesPage(data.map { it.toSAnime(titleLang) }, false)
} }
// =============================== Latest =============================== // =============================== Latest ===============================
@ -97,7 +106,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH) val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/api/anime/$id", headers)) client.newCall(GET("$baseUrl/anime/$id", headers))
.awaitSuccess() .awaitSuccess()
.use(::searchAnimeByIdParse) .use(::searchAnimeByIdParse)
} else { } else {
@ -130,15 +139,18 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}" override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
override fun animeDetailsRequest(anime: SAnime) = GET("$baseUrl/api${anime.url}", headers) override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
override fun animeDetailsParse(response: Response) = response.parseAs<ShortAnimeDto>().toSAnime(preferences.title) val data = document.parseAs<AnimePageDto>().animeData
return data.toSAnime(preferences.title)
}
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime) override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val anime = response.parseAs<LongAnimeDto>() val document = response.asJsoup()
val anime = document.parseAs<AnimePageDto>().animeData
return anime.episodes.map { return anime.episodes.map {
SEpisode.create().apply { SEpisode.create().apply {
name = it.title name = it.title
@ -155,8 +167,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.asJsoup()
val jsonString = document.selectFirst("script#__NEXT_DATA__")?.data() ?: return emptyList() val data = document.parseAs<EpisodePageDto>().episodeData
val data = json.decodeFromString<WatchDto>(jsonString).props.pageProps.episodeData
val subtitles = json.decodeFromString<List<SubtitleDto>>(data.subtitlesJson) val subtitles = json.decodeFromString<List<SubtitleDto>>(data.subtitlesJson)
// val videoUrl = client.newCall(GET("$baseUrl/api/streams?episodeId=${data.episode.id}", headers)).execute().parseAs<StreamsDto>().url // val videoUrl = client.newCall(GET("$baseUrl/api/streams?episodeId=${data.episode.id}", headers)).execute().parseAs<StreamsDto>().url
// keeping it in case the simpler solution breaks, can be hardcoded to this for now : // keeping it in case the simpler solution breaks, can be hardcoded to this for now :
@ -165,7 +176,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
videoUrl, videoUrl,
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" }, videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
subtitleList = subtitles.map { subtitleList = subtitles.map {
Track("$baseUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})") Track("$ipfsUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
}.sort(), }.sort(),
) )
} }
@ -233,6 +244,11 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
private inline fun <reified T> Document.parseAs(): T {
val nextData = this.selectFirst("script#__NEXT_DATA__")!!.data()
return json.decodeFromString<PropsDto<T>>(nextData).props.pageProps
}
private val SharedPreferences.quality get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! private val SharedPreferences.quality get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.subtitles get() = getString(PREF_SUBTITLES_KEY, PREF_SUBTITLES_DEFAULT)!! private val SharedPreferences.subtitles get() = getString(PREF_SUBTITLES_KEY, PREF_SUBTITLES_DEFAULT)!!
private val SharedPreferences.title get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!! private val SharedPreferences.title get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!

View File

@ -4,16 +4,23 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Genre(val name: String) data class GenreDto(val name: String)
@Serializable @Serializable
data class AnimeGenreRelation( data class AnimeGenreRelationDto(
@SerialName("Genre") @SerialName("Genre")
val genre: Genre, val genre: GenreDto,
) )
@Serializable @Serializable
data class ShortAnimeDto( data class EpisodeDto(
val title: String,
val id: Int,
val number: Int,
)
@Serializable
data class AnimeDto(
val titleRomanji: String?, val titleRomanji: String?,
val titleEnglish: String?, val titleEnglish: String?,
val titleJapanese: String?, val titleJapanese: String?,
@ -22,36 +29,46 @@ data class ShortAnimeDto(
val statusId: Int, val statusId: Int,
val imgUrl: String, val imgUrl: String,
@SerialName("AnimeGenres") @SerialName("AnimeGenres")
val animeGenres: List<AnimeGenreRelation>?, val animeGenres: List<AnimeGenreRelationDto>?,
@SerialName("Episodes")
val episodes: List<EpisodeDto> = emptyList(),
) )
@Serializable @Serializable
data class HomeListDto( data class HomePageDto(
@SerialName("AnimeSpotlight") @SerialName("AnimeSpotlight")
val animeSpotlight: List<ShortAnimeDto>, val animeSpotlight: List<AnimeDto>,
) )
@Serializable
data class AnimePageDto(
val animeData: AnimeDto,
)
@Serializable
data class EpisodeDataDto(
val episode: EpisodeDto,
val subtitlesJson: String,
)
@Serializable
data class EpisodePageDto(
val episodeData: EpisodeDataDto,
)
@Serializable
data class PagePropsDto<T>(val pageProps: T)
@Serializable
data class PropsDto<T>(val props: PagePropsDto<T>)
@Serializable @Serializable
data class DirectoryDto( data class DirectoryDto(
val animes: List<ShortAnimeDto>, val animes: List<AnimeDto>,
val page: Int, val page: Int,
val pages: Int, val pages: Int,
) )
@Serializable
data class Episode(
val title: String,
val id: Int,
val number: Int,
)
@Serializable
data class LongAnimeDto(
val slug: String,
@SerialName("Episodes")
val episodes: List<Episode>,
)
@Serializable @Serializable
data class SubtitleLangDto( data class SubtitleLangDto(
val name: String, val name: String,
@ -65,27 +82,6 @@ data class SubtitleDto(
val subtitlesName: SubtitleLangDto, val subtitlesName: SubtitleLangDto,
) )
@Serializable
data class EpisodeDataDto(
val episode: Episode,
val subtitlesJson: String,
)
@Serializable
data class PagePropsDto(
val episodeData: EpisodeDataDto,
)
@Serializable
data class DataWatchDto(
val pageProps: PagePropsDto,
)
@Serializable
data class WatchDto(
val props: DataWatchDto,
)
@Serializable @Serializable
data class FilterItemDto( data class FilterItemDto(
val id: Int, val id: Int,