feat(en/allanimechi): Add option to use hoster names & fix gogo (#2575)
This commit is contained in:
parent
b2a68f135a
commit
d60ff529e2
@ -8,7 +8,7 @@ ext {
|
|||||||
extName = 'AllAnimeChi'
|
extName = 'AllAnimeChi'
|
||||||
pkgNameSuffix = 'en.allanimechi'
|
pkgNameSuffix = 'en.allanimechi'
|
||||||
extClass = '.AllAnimeChi'
|
extClass = '.AllAnimeChi'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
libVersion = '13'
|
libVersion = '13'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ import kotlinx.serialization.json.putJsonArray
|
|||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -46,6 +47,7 @@ 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 uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
@ -252,8 +254,9 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
return GET(url, apiHeaders)
|
return GET(url, apiHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace with getAnimeUrl when new ext-lib is available
|
||||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||||
return GET("data:text/plain,This%20extension%20does%20not%20exist%20as%20a%20website%21")
|
return GET("data:text/plain,This%20extension%20does%20not%20have%20a%20website.")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun animeDetailsParse(response: Response): SAnime {
|
override fun animeDetailsParse(response: Response): SAnime {
|
||||||
@ -308,7 +311,7 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
|
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
private val internalExtractor by lazy { InternalExtractor(client, apiHeaders) }
|
private val internalExtractor by lazy { InternalExtractor(client, apiHeaders, headers) }
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
val variables = episode.url
|
val variables = episode.url
|
||||||
@ -334,6 +337,7 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
|
|
||||||
val hosterBlackList = preferences.getHosterBlacklist
|
val hosterBlackList = preferences.getHosterBlacklist
|
||||||
val altHosterBlackList = preferences.getAltHosterBlacklist
|
val altHosterBlackList = preferences.getAltHosterBlacklist
|
||||||
|
val useHosterNames = preferences.useHosterName
|
||||||
|
|
||||||
val serverList = videoJson.data.episode.sourceUrls.mapNotNull { video ->
|
val serverList = videoJson.data.episode.sourceUrls.mapNotNull { video ->
|
||||||
when {
|
when {
|
||||||
@ -359,26 +363,32 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return prioritySort(
|
return prioritySort(
|
||||||
serverList.parallelCatchingFlatMap(::getVideoFromServer),
|
serverList.parallelCatchingFlatMap { getVideoFromServer(it, useHosterNames) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVideoFromServer(server: Server): List<Pair<Video, Float>> {
|
private fun getVideoFromServer(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||||
return when (server.type) {
|
return when (server.type) {
|
||||||
"player" -> getFromPlayer(server)
|
"player" -> getFromPlayer(server, useHosterName)
|
||||||
"internal" -> internalExtractor.videosFromServer(server, removeRaw = preferences.removeRaw)
|
"internal" -> internalExtractor.videosFromServer(server, useHosterName, removeRaw = preferences.removeRaw)
|
||||||
"external" -> getFromExternal(server)
|
"external" -> getFromExternal(server, useHosterName)
|
||||||
else -> emptyList()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFromPlayer(server: Server): List<Pair<Video, Float>> {
|
private fun getFromPlayer(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||||
|
val name = if (useHosterName) {
|
||||||
|
getHostName(server.sourceUrl, server.sourceName)
|
||||||
|
} else {
|
||||||
|
server.sourceName
|
||||||
|
}
|
||||||
|
|
||||||
val videoHeaders = headers.newBuilder().apply {
|
val videoHeaders = headers.newBuilder().apply {
|
||||||
add("origin", siteUrl)
|
add("origin", siteUrl)
|
||||||
add("referer", "$siteUrl/")
|
add("referer", "$siteUrl/")
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val video = Video(server.sourceUrl, server.sourceName, server.sourceUrl, headers = videoHeaders)
|
val video = Video(server.sourceUrl, name, server.sourceUrl, headers = videoHeaders)
|
||||||
return listOf(
|
return listOf(
|
||||||
Pair(video, server.priority),
|
Pair(video, server.priority),
|
||||||
)
|
)
|
||||||
@ -392,9 +402,14 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
private val allanimeExtractor by lazy { AllAnimeExtractor(client, headers) }
|
private val allanimeExtractor by lazy { AllAnimeExtractor(client, headers) }
|
||||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||||
|
|
||||||
private fun getFromExternal(server: Server): List<Pair<Video, Float>> {
|
private fun getFromExternal(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||||
val url = server.sourceUrl.replace(Regex("""^//"""), "https://")
|
val url = server.sourceUrl.replace(Regex("""^//"""), "https://")
|
||||||
val prefix = "${server.sourceName} - "
|
val prefix = if (useHosterName) {
|
||||||
|
"${getHostName(url, server.sourceName)} - "
|
||||||
|
} else {
|
||||||
|
"${server.sourceName} - "
|
||||||
|
}
|
||||||
|
|
||||||
val videoList = when {
|
val videoList = when {
|
||||||
url.startsWith("https://ok") -> okruExtractor.videosFromUrl(url, prefix = prefix)
|
url.startsWith("https://ok") -> okruExtractor.videosFromUrl(url, prefix = prefix)
|
||||||
url.startsWith("https://filemoon") -> filemoonExtractor.videosFromUrl(url, prefix = prefix)
|
url.startsWith("https://filemoon") -> filemoonExtractor.videosFromUrl(url, prefix = prefix)
|
||||||
@ -411,6 +426,14 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private fun getHostName(host: String, fallback: String): String {
|
||||||
|
return host.toHttpUrlOrNull()?.host?.split(".")?.let {
|
||||||
|
it.getOrNull(it.size - 2)?.replaceFirstChar { c ->
|
||||||
|
if (c.isLowerCase()) c.titlecase(Locale.ROOT) else c.toString()
|
||||||
|
}
|
||||||
|
} ?: fallback
|
||||||
|
}
|
||||||
|
|
||||||
private fun String.decodeBase64(): String {
|
private fun String.decodeBase64(): String {
|
||||||
return String(Base64.decode(this, Base64.DEFAULT))
|
return String(Base64.decode(this, Base64.DEFAULT))
|
||||||
}
|
}
|
||||||
@ -468,6 +491,8 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val PAGE_SIZE = 30 // number of items to retrieve when calling API
|
||||||
|
|
||||||
private const val POPULAR_HASH = "31a117653812a2547fd981632e8c99fa8bf8a75c4ef1a77a1567ef1741a7ab9c"
|
private const val POPULAR_HASH = "31a117653812a2547fd981632e8c99fa8bf8a75c4ef1a77a1567ef1741a7ab9c"
|
||||||
private const val LATEST_HASH = "e42a4466d984b2c0a2cecae5dd13aa68867f634b16ee0f17b380047d14482406"
|
private const val LATEST_HASH = "e42a4466d984b2c0a2cecae5dd13aa68867f634b16ee0f17b380047d14482406"
|
||||||
private const val DETAILS_HASH = "bb263f91e5bdd048c1c978f324613aeccdfe2cbc694a419466a31edb58c0cc0b"
|
private const val DETAILS_HASH = "bb263f91e5bdd048c1c978f324613aeccdfe2cbc694a419466a31edb58c0cc0b"
|
||||||
@ -492,8 +517,6 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
"Yt-mp4",
|
"Yt-mp4",
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val PAGE_SIZE = 30 // number of items to retrieve when calling API
|
|
||||||
|
|
||||||
private const val PREF_HOSTER_BLACKLIST_KEY = "pref_hoster_blacklist"
|
private const val PREF_HOSTER_BLACKLIST_KEY = "pref_hoster_blacklist"
|
||||||
private val PREF_HOSTER_BLACKLIST_ENTRY_VALUES = INTERNAL_HOSTER_NAMES.map {
|
private val PREF_HOSTER_BLACKLIST_ENTRY_VALUES = INTERNAL_HOSTER_NAMES.map {
|
||||||
it.lowercase().substringBefore(" (")
|
it.lowercase().substringBefore(" (")
|
||||||
@ -542,6 +565,9 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
|
|
||||||
private const val PREF_SUB_KEY = "preferred_sub"
|
private const val PREF_SUB_KEY = "preferred_sub"
|
||||||
private const val PREF_SUB_DEFAULT = "sub"
|
private const val PREF_SUB_DEFAULT = "sub"
|
||||||
|
|
||||||
|
private const val PREF_USE_HOSTER_NAMES_KEY = "use_host_prefix"
|
||||||
|
private const val PREF_USE_HOSTER_NAMES_DEFAULT = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
// ============================== Settings ==============================
|
||||||
@ -588,17 +614,6 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = PREF_REMOVE_RAW_KEY
|
|
||||||
title = "Attempt to filter out raw"
|
|
||||||
setDefaultValue(PREF_REMOVE_RAW_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val new = newValue as Boolean
|
|
||||||
preferences.edit().putBoolean(key, new).commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_QUALITY_KEY
|
key = PREF_QUALITY_KEY
|
||||||
title = "Preferred quality"
|
title = "Preferred quality"
|
||||||
@ -646,6 +661,28 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
preferences.edit().putString(key, entry).commit()
|
preferences.edit().putString(key, entry).commit()
|
||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = PREF_REMOVE_RAW_KEY
|
||||||
|
title = "Attempt to filter out raw"
|
||||||
|
setDefaultValue(PREF_REMOVE_RAW_DEFAULT)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val new = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean(key, new).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = PREF_USE_HOSTER_NAMES_KEY
|
||||||
|
title = "Use names of video hoster"
|
||||||
|
setDefaultValue(PREF_USE_HOSTER_NAMES_DEFAULT)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val new = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean(key, new).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SharedPreferences.subPref
|
private val SharedPreferences.subPref
|
||||||
@ -668,4 +705,7 @@ class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
|
|
||||||
private val SharedPreferences.removeRaw
|
private val SharedPreferences.removeRaw
|
||||||
get() = getBoolean(PREF_REMOVE_RAW_KEY, PREF_REMOVE_RAW_DEFAULT)
|
get() = getBoolean(PREF_REMOVE_RAW_KEY, PREF_REMOVE_RAW_DEFAULT)
|
||||||
|
|
||||||
|
private val SharedPreferences.useHosterName
|
||||||
|
get() = getBoolean(PREF_USE_HOSTER_NAMES_KEY, PREF_USE_HOSTER_NAMES_DEFAULT)
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,13 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class InternalExtractor(private val client: OkHttpClient, private val apiHeaders: Headers) {
|
class InternalExtractor(private val client: OkHttpClient, private val apiHeaders: Headers, private val headers: Headers) {
|
||||||
|
|
||||||
private val blogUrl = "aHR0cHM6Ly9ibG9nLmFsbGFuaW1lLnBybw==".decodeBase64()
|
private val blogUrl = "aHR0cHM6Ly9ibG9nLmFsbGFuaW1lLnBybw==".decodeBase64()
|
||||||
|
|
||||||
@ -22,11 +24,11 @@ class InternalExtractor(private val client: OkHttpClient, private val apiHeaders
|
|||||||
"Dalvik/2.1.0 (Linux; U; Android 13; Pixel 5 Build/TQ3A.230705.001.B4)",
|
"Dalvik/2.1.0 (Linux; U; Android 13; Pixel 5 Build/TQ3A.230705.001.B4)",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, playlistHeaders) }
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
fun videosFromServer(server: AllAnimeChi.Server, removeRaw: Boolean): List<Pair<Video, Float>> {
|
fun videosFromServer(server: AllAnimeChi.Server, useHosterName: Boolean, removeRaw: Boolean): List<Pair<Video, Float>> {
|
||||||
val blogHeaders = apiHeaders.newBuilder().apply {
|
val blogHeaders = apiHeaders.newBuilder().apply {
|
||||||
set("host", blogUrl.toHttpUrl().host)
|
set("host", blogUrl.toHttpUrl().host)
|
||||||
}.build()
|
}.build()
|
||||||
@ -37,8 +39,8 @@ class InternalExtractor(private val client: OkHttpClient, private val apiHeaders
|
|||||||
|
|
||||||
val videoList = videoData.links.flatMap {
|
val videoList = videoData.links.flatMap {
|
||||||
when {
|
when {
|
||||||
it.hls == true -> getFromHls(server, it, removeRaw)
|
it.hls == true -> getFromHls(server, it, useHosterName, removeRaw)
|
||||||
it.mp4 == true -> getFromMp4(server, it)
|
it.mp4 == true -> getFromMp4(server, it, useHosterName)
|
||||||
else -> emptyList()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,20 +48,26 @@ class InternalExtractor(private val client: OkHttpClient, private val apiHeaders
|
|||||||
return videoList
|
return videoList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFromMp4(server: AllAnimeChi.Server, data: VideoData.LinkObject): List<Pair<Video, Float>> {
|
private fun getFromMp4(server: AllAnimeChi.Server, data: VideoData.LinkObject, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||||
val baseName = "${server.sourceName} - ${data.resolutionStr}"
|
val host = if (useHosterName) getHostName(data.link, server.sourceName) else server.sourceName
|
||||||
|
|
||||||
|
val baseName = "$host - ${data.resolutionStr}"
|
||||||
val video = Video(data.link, baseName, data.link, headers = playlistHeaders)
|
val video = Video(data.link, baseName, data.link, headers = playlistHeaders)
|
||||||
return listOf(
|
return listOf(
|
||||||
Pair(video, server.priority),
|
Pair(video, server.priority),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFromHls(server: AllAnimeChi.Server, data: VideoData.LinkObject, removeRaw: Boolean): List<Pair<Video, Float>> {
|
private fun getFromHls(server: AllAnimeChi.Server, data: VideoData.LinkObject, useHosterName: Boolean, removeRaw: Boolean): List<Pair<Video, Float>> {
|
||||||
if (removeRaw && data.resolutionStr.contains("raw", true)) return emptyList()
|
if (removeRaw && data.resolutionStr.contains("raw", true)) return emptyList()
|
||||||
|
val host = if (useHosterName) getHostName(data.link, server.sourceName) else server.sourceName
|
||||||
|
|
||||||
val linkHost = data.link.toHttpUrl().host
|
val linkHost = data.link.toHttpUrl().host
|
||||||
|
|
||||||
// Doesn't seem to work
|
// Doesn't seem to work
|
||||||
if (server.sourceName.equals("Luf-mp4", true) && linkHost.contains("maverickki")) return emptyList()
|
if (server.sourceName.equals("Luf-mp4", true)) {
|
||||||
|
return getFromGogo(server, data, host)
|
||||||
|
}
|
||||||
|
|
||||||
// Hardcode some names
|
// Hardcode some names
|
||||||
val baseName = if (linkHost.contains("crunchyroll")) {
|
val baseName = if (linkHost.contains("crunchyroll")) {
|
||||||
@ -73,8 +81,7 @@ class InternalExtractor(private val client: OkHttpClient, private val apiHeaders
|
|||||||
.replace("vo_SUB", "Sub")
|
.replace("vo_SUB", "Sub")
|
||||||
.replace("SUB", "Sub")
|
.replace("SUB", "Sub")
|
||||||
} else {
|
} else {
|
||||||
"${server.sourceName} - ${data.resolutionStr}"
|
"$host - ${data.resolutionStr}"
|
||||||
.replace("Luf-mp4", "Luf-mp4 (gogo)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get stuff
|
// Get stuff
|
||||||
@ -91,7 +98,7 @@ class InternalExtractor(private val client: OkHttpClient, private val apiHeaders
|
|||||||
|
|
||||||
val videoList = playlistUtils.extractFromHls(
|
val videoList = playlistUtils.extractFromHls(
|
||||||
data.link,
|
data.link,
|
||||||
videoNameGen = { q -> "$baseName - $q" },
|
videoNameGen = { q -> "$baseName - ${data.resolutionStr} - $q" },
|
||||||
masterHeadersGen = { _, _ -> masterHeaders },
|
masterHeadersGen = { _, _ -> masterHeaders },
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,8 +107,33 @@ class InternalExtractor(private val client: OkHttpClient, private val apiHeaders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getFromGogo(server: AllAnimeChi.Server, data: VideoData.LinkObject, hostName: String): List<Pair<Video, Float>> {
|
||||||
|
val host = data.link.toHttpUrl().host
|
||||||
|
|
||||||
|
// Seems to be dead
|
||||||
|
if (host.contains("maverickki", true)) return emptyList()
|
||||||
|
|
||||||
|
val videoList = playlistUtils.extractFromHls(
|
||||||
|
data.link,
|
||||||
|
videoNameGen = { q -> "$hostName - ${data.resolutionStr} - $q" },
|
||||||
|
referer = "https://playtaku.net/",
|
||||||
|
)
|
||||||
|
|
||||||
|
return videoList.map {
|
||||||
|
Pair(it, server.priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private fun getHostName(host: String, fallback: String): String {
|
||||||
|
return host.toHttpUrlOrNull()?.host?.split(".")?.let {
|
||||||
|
it.getOrNull(it.size - 2)?.replaceFirstChar { c ->
|
||||||
|
if (c.isLowerCase()) c.titlecase(Locale.ROOT) else c.toString()
|
||||||
|
}
|
||||||
|
} ?: fallback
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class VideoData(
|
data class VideoData(
|
||||||
val links: List<LinkObject>,
|
val links: List<LinkObject>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user