en/seez dead
This commit is contained in:
parent
fd6d507ed2
commit
623db51bd8
9 changed files with 0 additions and 568 deletions
|
@ -1,12 +0,0 @@
|
|||
ext {
|
||||
extName = 'Seez'
|
||||
extClass = '.Seez'
|
||||
extVersionCode = 11
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:vidsrc-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
|
@ -1,434 +0,0 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.seez
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.vidsrcextractor.VidsrcExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLDecoder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class Seez : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "Seez"
|
||||
|
||||
override val baseUrl = "https://seez.su"
|
||||
|
||||
private val embedUrl = "https://vidsrc.to"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val apiKey by lazy {
|
||||
val jsUrl = client.newCall(GET(baseUrl, headers)).execute().asJsoup()
|
||||
.select("script[defer][src]")[1].attr("abs:src")
|
||||
|
||||
val jsBody = client.newCall(GET(jsUrl, headers)).execute().body.string()
|
||||
Regex("""f="(\w{20,})"""").find(jsBody)!!.groupValues[1]
|
||||
}
|
||||
|
||||
private val apiHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", "api.themoviedb.org")
|
||||
add("Origin", baseUrl)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
val url = TMDB_URL.newBuilder().apply {
|
||||
addPathSegment("movie")
|
||||
addPathSegment("popular")
|
||||
addQueryParameter("language", "en-US")
|
||||
addQueryParameter("page", page.toString())
|
||||
}.buildAPIUrl()
|
||||
|
||||
return GET(url, headers = apiHeaders)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val data = response.parseAs<TmdbResponse>()
|
||||
|
||||
val animeList = data.results.map { ani ->
|
||||
val name = ani.title ?: ani.name ?: "Title N/A"
|
||||
|
||||
SAnime.create().apply {
|
||||
title = name
|
||||
url = LinkData(ani.id, "movie").toJsonString()
|
||||
thumbnail_url = ani.posterPath?.let { IMG_URL + it } ?: FALLBACK_IMG
|
||||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, data.page < data.totalPages)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage = throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val typeFilter = filterList.find { it is TypeFilter } as TypeFilter
|
||||
val collectionFilter = filterList.find { it is CollectionFilter } as CollectionFilter
|
||||
val orderFilter = filterList.find { it is OrderFilter } as OrderFilter
|
||||
|
||||
val url = if (query.isNotBlank()) {
|
||||
TMDB_URL.newBuilder().apply {
|
||||
addPathSegment("search")
|
||||
addPathSegment("multi")
|
||||
addQueryParameter("query", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}.buildAPIUrl()
|
||||
} else {
|
||||
TMDB_URL.newBuilder().apply {
|
||||
addPathSegment(typeFilter.toUriPart())
|
||||
addPathSegment(orderFilter.toUriPart())
|
||||
if (collectionFilter.state != 0) {
|
||||
addQueryParameter("with_networks", collectionFilter.toUriPart())
|
||||
}
|
||||
addQueryParameter("language", "en-US")
|
||||
addQueryParameter("page", page.toString())
|
||||
}.buildAPIUrl()
|
||||
}
|
||||
|
||||
return GET(url, headers = apiHeaders)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val data = response.parseAs<TmdbResponse>()
|
||||
|
||||
val animeList = data.results.map { ani ->
|
||||
val name = ani.title ?: ani.name ?: "Title N/A"
|
||||
|
||||
SAnime.create().apply {
|
||||
title = name
|
||||
url = LinkData(ani.id, ani.mediaType).toJsonString()
|
||||
thumbnail_url = ani.posterPath?.let { IMG_URL + it } ?: FALLBACK_IMG
|
||||
status = if (ani.mediaType == "movie") SAnime.COMPLETED else SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, data.page < data.totalPages)
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("NOTE: Filters are going to be ignored if using search text"),
|
||||
TypeFilter(),
|
||||
CollectionFilter(),
|
||||
OrderFilter(),
|
||||
)
|
||||
|
||||
private class TypeFilter : UriPartFilter(
|
||||
"Type",
|
||||
arrayOf(
|
||||
Pair("Movies", "movie"),
|
||||
Pair("TV-shows", "tv"),
|
||||
),
|
||||
)
|
||||
|
||||
private class CollectionFilter : UriPartFilter(
|
||||
"Collection",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Netflix", "213"),
|
||||
Pair("HBO Max", "49"),
|
||||
Pair("Paramount+", "4330"),
|
||||
Pair("Disney+", "2739"),
|
||||
Pair("Apple TV+", "2552"),
|
||||
Pair("Prime Video", "1024"),
|
||||
),
|
||||
)
|
||||
|
||||
private class OrderFilter : UriPartFilter(
|
||||
"Order by",
|
||||
arrayOf(
|
||||
Pair("Popular", "popular"),
|
||||
Pair("Top", "top_rated"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
val data = json.decodeFromString<LinkData>(anime.url)
|
||||
|
||||
val url = TMDB_URL.newBuilder().apply {
|
||||
addPathSegment(data.mediaType)
|
||||
addPathSegment(data.id.toString())
|
||||
}.buildAPIUrl()
|
||||
|
||||
return GET(url, headers = apiHeaders)
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val data = response.parseAs<TmdbDetailsResponse>()
|
||||
|
||||
return SAnime.create().apply {
|
||||
author = data.productions?.joinToString { it.name }
|
||||
genre = data.genres?.joinToString { it.name }
|
||||
status = when (data.status) {
|
||||
"Ended", "Released" -> SAnime.COMPLETED
|
||||
"In Production" -> SAnime.LICENSED
|
||||
"Canceled" -> SAnime.CANCELLED
|
||||
"Returning Series" -> {
|
||||
data.nextEpisode?.let { SAnime.ONGOING } ?: SAnime.ON_HIATUS
|
||||
}
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
description = buildString {
|
||||
data.overview?.let {
|
||||
appendLine(it)
|
||||
appendLine()
|
||||
}
|
||||
data.nextEpisode?.let {
|
||||
appendLine("Next: Ep ${it.epNumber} - ${it.name}")
|
||||
appendLine("Air Date: ${it.airDate}")
|
||||
appendLine()
|
||||
}
|
||||
data.releaseDate?.let { appendLine("Release date: $it") }
|
||||
data.firstAirDate?.let { appendLine("First air date: $it") }
|
||||
data.lastAirDate?.let { appendLine("Last air date: $it") }
|
||||
data.languages?.let { langs ->
|
||||
append("Languages: ")
|
||||
appendLine(langs.joinToString { "${it.engName} (${it.name})" })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request = animeDetailsRequest(anime)
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val data = response.parseAs<TmdbDetailsResponse>()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
|
||||
if (data.title != null) { // movie
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = "Movie"
|
||||
date_upload = parseDate(data.releaseDate!!)
|
||||
episode_number = 1F
|
||||
url = "/movie/${data.id}"
|
||||
},
|
||||
)
|
||||
} else {
|
||||
data.seasons.filter { t -> t.seasonNumber != 0 }.forEach { season ->
|
||||
val seasonUrl = TMDB_URL.newBuilder().apply {
|
||||
addPathSegment("tv")
|
||||
addPathSegment(data.id.toString())
|
||||
addPathSegment("season")
|
||||
addPathSegment(season.seasonNumber.toString())
|
||||
}.buildAPIUrl()
|
||||
|
||||
val seasonData = client.newCall(
|
||||
GET(seasonUrl, headers = apiHeaders),
|
||||
).execute().parseAs<TmdbSeasonResponse>()
|
||||
|
||||
seasonData.episodes.filter { ep ->
|
||||
ep.airDate?.let {
|
||||
DATE_FORMATTER.parse(it)!! <= DATE_FORMATTER.parse(DATE_FORMATTER.format(Date()))
|
||||
} ?: false
|
||||
}.forEach { ep ->
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = "Season ${season.seasonNumber} Ep. ${ep.epNumber} - ${ep.name}"
|
||||
date_upload = ep.airDate?.let(::parseDate) ?: 0L
|
||||
episode_number = ep.epNumber.toFloat()
|
||||
url = "/tv/${data.id}/${season.seasonNumber}/${ep.epNumber}"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
private val vidsrcExtractor by lazy { VidsrcExtractor(client, headers) }
|
||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val docHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
add("Host", embedUrl.toHttpUrl().host)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
return GET("$embedUrl/embed${episode.url}", headers = docHeaders)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val sourcesHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", embedUrl.toHttpUrl().host)
|
||||
add("Referer", response.request.url.toString())
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
val dataId = document.selectFirst("ul.episodes li a[data-id]")!!.attr("data-id")
|
||||
val sources = client.newCall(
|
||||
GET("$embedUrl/ajax/embed/episode/$dataId/sources", headers = sourcesHeaders),
|
||||
).execute().parseAs<EmbedSourceList>().result
|
||||
|
||||
val urlList = sources.map {
|
||||
val encrypted = client.newCall(
|
||||
GET("$embedUrl/ajax/embed/source/${it.id}", headers = sourcesHeaders),
|
||||
).execute().parseAs<EmbedUrlResponse>().result.url
|
||||
|
||||
Pair(decrypt(encrypted), it.title)
|
||||
}
|
||||
|
||||
return urlList.parallelCatchingFlatMapBlocking {
|
||||
val url = it.first
|
||||
|
||||
when (val name = it.second) {
|
||||
"Vidplay" -> vidsrcExtractor.videosFromUrl(url, name)
|
||||
"Filemoon" -> filemoonExtractor.videosFromUrl(url)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private fun HttpUrl.Builder.buildAPIUrl(): String = this.apply {
|
||||
addQueryParameter("api_key", apiKey)
|
||||
}.build().toString()
|
||||
|
||||
private fun LinkData.toJsonString(): String {
|
||||
return json.encodeToString(this)
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
private fun decrypt(encrypted: String): String {
|
||||
var vrf = encrypted.toByteArray()
|
||||
vrf = Base64.decode(vrf, Base64.URL_SAFE)
|
||||
|
||||
val rc4Key = SecretKeySpec("8z5Ag5wgagfsOuhz".toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
vrf = cipher.doFinal(vrf)
|
||||
|
||||
return URLDecoder.decode(vrf.toString(Charsets.UTF_8), "utf-8")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TMDB_URL = "https://api.themoviedb.org/3".toHttpUrl()
|
||||
private const val IMG_URL = "https://image.tmdb.org/t/p/w300/"
|
||||
private const val FALLBACK_IMG = "https://seez.su/fallback.png"
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Vidplay"
|
||||
}
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = arrayOf("Vidplay", "Filemoon")
|
||||
entryValues = arrayOf("Vidplay", "Filemoon")
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.seez
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TmdbResponse(
|
||||
val page: Int,
|
||||
@SerialName("total_pages")
|
||||
val totalPages: Int,
|
||||
val results: List<TmdbResult>,
|
||||
) {
|
||||
@Serializable
|
||||
data class TmdbResult(
|
||||
val id: Int,
|
||||
@SerialName("media_type")
|
||||
val mediaType: String = "tv",
|
||||
@SerialName("poster_path")
|
||||
val posterPath: String? = null,
|
||||
val title: String? = null,
|
||||
val name: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TmdbDetailsResponse(
|
||||
val id: Int,
|
||||
val overview: String? = null,
|
||||
val genres: List<GenreObject>? = null,
|
||||
@SerialName("release_date")
|
||||
val releaseDate: String? = null,
|
||||
@SerialName("first_air_date")
|
||||
val firstAirDate: String? = null,
|
||||
@SerialName("last_air_date")
|
||||
val lastAirDate: String? = null,
|
||||
val name: String? = null,
|
||||
val title: String? = null,
|
||||
val seasons: List<SeasonObject> = emptyList(),
|
||||
val status: String,
|
||||
@SerialName("next_episode_to_air")
|
||||
val nextEpisode: NextEpisode? = null,
|
||||
@SerialName("production_companies")
|
||||
val productions: List<Company>? = null,
|
||||
@SerialName("spoken_languages")
|
||||
val languages: List<Language>? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class GenreObject(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SeasonObject(
|
||||
@SerialName("season_number")
|
||||
val seasonNumber: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NextEpisode(
|
||||
val name: String? = "",
|
||||
@SerialName("episode_number")
|
||||
val epNumber: Int,
|
||||
@SerialName("air_date")
|
||||
val airDate: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Company(
|
||||
val name: String,
|
||||
@SerialName("origin_country")
|
||||
val originCountry: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Language(
|
||||
val name: String,
|
||||
@SerialName("english_name")
|
||||
val engName: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TmdbSeasonResponse(
|
||||
val episodes: List<EpisodeObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class EpisodeObject(
|
||||
@SerialName("episode_number")
|
||||
val epNumber: Int,
|
||||
val name: String,
|
||||
@SerialName("air_date")
|
||||
val airDate: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LinkData(
|
||||
val id: Int,
|
||||
@SerialName("media_type")
|
||||
val mediaType: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EmbedSourceList(
|
||||
val result: List<EmbedSource>,
|
||||
) {
|
||||
@Serializable
|
||||
data class EmbedSource(
|
||||
val id: String,
|
||||
val title: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EmbedUrlResponse(
|
||||
val result: EmbedUrlObject,
|
||||
) {
|
||||
@Serializable
|
||||
data class EmbedUrlObject(
|
||||
val url: String,
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue