mirror of
https://github.com/recloudstream/cloudstream.git
synced 2025-05-17 19:25:55 +08:00
Move metaproviders to library (#1541)
* Move metaproviders to library So that providers can extend them when using the library dependency. * Remove gson
This commit is contained in:
@ -31,6 +31,7 @@ kotlin {
|
||||
implementation(libs.fuzzywuzzy) // Match Extractors
|
||||
implementation(libs.rhino) // Run JavaScript
|
||||
implementation(libs.newpipeextractor)
|
||||
implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,119 @@
|
||||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.MovieLoadResponse
|
||||
import com.lagradost.cloudstream3.MovieSearchResponse
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
||||
class CrossTmdbProvider : TmdbProvider() {
|
||||
override var name = "MultiMovie"
|
||||
override val apiName = "MultiMovie"
|
||||
override var lang = "en"
|
||||
override val useMetaLoadResponse = true
|
||||
override val usesWebView = true
|
||||
override val supportedTypes = setOf(TvType.Movie)
|
||||
|
||||
private fun filterName(name: String): String {
|
||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||
}
|
||||
|
||||
private val validApis
|
||||
get() =
|
||||
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
|
||||
//.distinctBy { it.uniqueId }
|
||||
|
||||
|
||||
data class CrossMetaData(
|
||||
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
||||
@JsonProperty("movies") val movies: List<Pair<String, String>>? = null,
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
tryParseJson<CrossMetaData>(data)?.let { metaData ->
|
||||
if (!metaData.isSuccess) return false
|
||||
metaData.movies?.amap { (apiName, data) ->
|
||||
getApiFromNameNull(apiName)?.let {
|
||||
try {
|
||||
it.loadLinks(data, isCasting, subtitleCallback, callback)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
return super.search(query)?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val base = super.load(url)?.apply {
|
||||
this.recommendations =
|
||||
this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||
val matchName = filterName(this.name)
|
||||
when (this) {
|
||||
is MovieLoadResponse -> {
|
||||
val data = validApis.amap { api ->
|
||||
try {
|
||||
if (api.supportedTypes.contains(TvType.Movie)) { //|| api.supportedTypes.contains(TvType.AnimeMovie)
|
||||
return@amap api.search(this.name)?.first {
|
||||
if (filterName(it.name).equals(
|
||||
matchName,
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
if (it is MovieSearchResponse)
|
||||
if (it.year != null && this.year != null && it.year != this.year) // if year exist then do a check
|
||||
return@first false
|
||||
|
||||
return@first true
|
||||
}
|
||||
false
|
||||
}?.let { search ->
|
||||
val response = api.load(search.url)
|
||||
if (response is MovieLoadResponse) {
|
||||
response
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
this.dataUrl =
|
||||
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
|
||||
object SyncRedirector {
|
||||
private val syncIds =
|
||||
listOf(
|
||||
SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
|
||||
SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
|
||||
)
|
||||
|
||||
suspend fun redirect(
|
||||
url: String,
|
||||
providerApi: MainAPI
|
||||
): String {
|
||||
// Deprecated since providers should do this instead!
|
||||
|
||||
// Tries built in ID -> ProviderUrl
|
||||
/*
|
||||
for (api in syncApis) {
|
||||
if (url.contains(api.mainUrl)) {
|
||||
val otherApi = when (api.name) {
|
||||
aniListApi.name -> "anilist"
|
||||
malApi.name -> "myanimelist"
|
||||
else -> return url
|
||||
}
|
||||
|
||||
SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
|
||||
realUrl.contains(providerApi.mainUrl)
|
||||
}?.let {
|
||||
return it
|
||||
}
|
||||
// ?: run {
|
||||
// throw ErrorLoadingException("Page does not exist on $preferredUrl")
|
||||
// }
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Tries provider solution
|
||||
// This goes through all sync ids and finds supported id by said provider
|
||||
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
|
||||
if (providerApi.supportedSyncNames.contains(syncName)) {
|
||||
syncRegex.find(url)?.value?.let {
|
||||
suspendSafeApiCall {
|
||||
providerApi.getLoadUrl(syncName, it)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
} ?: url
|
||||
}
|
||||
}
|
@ -0,0 +1,421 @@
|
||||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.Actor
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
import com.lagradost.cloudstream3.HomePageResponse
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.MainPageRequest
|
||||
import com.lagradost.cloudstream3.MovieLoadResponse
|
||||
import com.lagradost.cloudstream3.MovieSearchResponse
|
||||
import com.lagradost.cloudstream3.ProviderType
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.TvSeriesLoadResponse
|
||||
import com.lagradost.cloudstream3.TvSeriesSearchResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.argamap
|
||||
import com.lagradost.cloudstream3.newEpisode
|
||||
import com.lagradost.cloudstream3.newHomePageResponse
|
||||
import com.lagradost.cloudstream3.newMovieLoadResponse
|
||||
import com.lagradost.cloudstream3.newMovieSearchResponse
|
||||
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
|
||||
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.uwetrottmann.tmdb2.Tmdb
|
||||
import com.uwetrottmann.tmdb2.entities.AppendToResponse
|
||||
import com.uwetrottmann.tmdb2.entities.BaseMovie
|
||||
import com.uwetrottmann.tmdb2.entities.BaseTvShow
|
||||
import com.uwetrottmann.tmdb2.entities.CastMember
|
||||
import com.uwetrottmann.tmdb2.entities.ContentRating
|
||||
import com.uwetrottmann.tmdb2.entities.Movie
|
||||
import com.uwetrottmann.tmdb2.entities.ReleaseDate
|
||||
import com.uwetrottmann.tmdb2.entities.ReleaseDatesResult
|
||||
import com.uwetrottmann.tmdb2.entities.TvSeason
|
||||
import com.uwetrottmann.tmdb2.entities.TvShow
|
||||
import com.uwetrottmann.tmdb2.entities.Videos
|
||||
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
|
||||
import com.uwetrottmann.tmdb2.enumerations.VideoType
|
||||
import retrofit2.awaitResponse
|
||||
import java.util.Calendar
|
||||
|
||||
/**
|
||||
* episode and season starting from 1
|
||||
* they are null if movie
|
||||
* */
|
||||
data class TmdbLink(
|
||||
@JsonProperty("imdbID") val imdbID: String?,
|
||||
@JsonProperty("tmdbID") val tmdbID: Int?,
|
||||
@JsonProperty("episode") val episode: Int?,
|
||||
@JsonProperty("season") val season: Int?,
|
||||
@JsonProperty("movieName") val movieName: String? = null,
|
||||
)
|
||||
|
||||
open class TmdbProvider : MainAPI() {
|
||||
// This should always be false, but might as well make it easier for forks
|
||||
open val includeAdult = false
|
||||
|
||||
// Use the LoadResponse from the metadata provider
|
||||
open val useMetaLoadResponse = false
|
||||
open val apiName = "TMDB"
|
||||
|
||||
// As some sites doesn't support s0
|
||||
open val disableSeasonZero = true
|
||||
|
||||
override val hasMainPage = true
|
||||
override val providerType = ProviderType.MetaProvider
|
||||
|
||||
// Fuck it, public private api key because github actions won't co-operate.
|
||||
// Please no stealy.
|
||||
private val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb")
|
||||
|
||||
private fun getImageUrl(link: String?): String? {
|
||||
if (link == null) return null
|
||||
return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500/$link" else link
|
||||
}
|
||||
|
||||
private fun getUrl(id: Int?, tvShow: Boolean): String {
|
||||
return if (tvShow) "https://www.themoviedb.org/tv/${id ?: -1}"
|
||||
else "https://www.themoviedb.org/movie/${id ?: -1}"
|
||||
}
|
||||
|
||||
private fun BaseTvShow.toSearchResponse(): TvSeriesSearchResponse {
|
||||
return newTvSeriesSearchResponse(
|
||||
name = this.name ?: this.original_name,
|
||||
url = getUrl(id, true),
|
||||
type = TvType.TvSeries,
|
||||
fix = false
|
||||
) {
|
||||
this.id = this@toSearchResponse.id
|
||||
this.posterUrl = getImageUrl(poster_path)
|
||||
this.year = first_air_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BaseMovie.toSearchResponse(): MovieSearchResponse {
|
||||
return newMovieSearchResponse(
|
||||
name = this.title ?: this.original_title,
|
||||
url = getUrl(id, false),
|
||||
type = TvType.Movie,
|
||||
fix = false
|
||||
) {
|
||||
this.id = this@toSearchResponse.id
|
||||
this.posterUrl = getImageUrl(poster_path)
|
||||
this.year = release_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<CastMember?>?.toActors(): List<Pair<Actor, String?>>? {
|
||||
return this?.mapNotNull {
|
||||
Pair(
|
||||
Actor(it?.name ?: return@mapNotNull null, getImageUrl(it.profile_path)),
|
||||
it.character
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TvShow.toLoadResponse(): TvSeriesLoadResponse {
|
||||
val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 }
|
||||
?.mapNotNull { season ->
|
||||
season.episodes?.map { episode ->
|
||||
newEpisode(
|
||||
TmdbLink(
|
||||
episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id,
|
||||
this.id,
|
||||
episode.episode_number,
|
||||
episode.season_number,
|
||||
this.name ?: this.original_name,
|
||||
).toJson()
|
||||
) {
|
||||
this.name = episode.name
|
||||
this.season = episode.season_number
|
||||
this.episode = episode.episode_number
|
||||
this.rating = episode.rating
|
||||
this.description = episode.overview
|
||||
this.date = episode.air_date?.time
|
||||
this.posterUrl = getImageUrl(episode.still_path)
|
||||
}
|
||||
} ?: (1..(season.episode_count ?: 1)).map { episodeNum ->
|
||||
newEpisode(
|
||||
TmdbLink(
|
||||
this.external_ids?.imdb_id,
|
||||
this.id,
|
||||
episodeNum,
|
||||
season.season_number,
|
||||
this.name ?: this.original_name,
|
||||
).toJson()
|
||||
) {
|
||||
this.episode = episodeNum
|
||||
this.season = season.season_number
|
||||
}
|
||||
}
|
||||
}?.flatten() ?: listOf()
|
||||
|
||||
return newTvSeriesLoadResponse(
|
||||
this.name ?: this.original_name,
|
||||
getUrl(id, true),
|
||||
TvType.TvSeries,
|
||||
episodes
|
||||
) {
|
||||
posterUrl = getImageUrl(poster_path)
|
||||
year = first_air_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
plot = overview
|
||||
addImdbId(external_ids?.imdb_id)
|
||||
|
||||
tags = genres?.mapNotNull { it.name }
|
||||
duration = episode_run_time?.average()?.toInt()
|
||||
rating = this@toLoadResponse.rating
|
||||
addTrailer(videos.toTrailers())
|
||||
|
||||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
addActors(credits?.cast?.toList().toActors())
|
||||
|
||||
contentRating = fetchContentRating(id, "US")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Videos?.toTrailers(): List<String>? {
|
||||
return this?.results?.filter { it.type != VideoType.OPENING_CREDITS && it.type != VideoType.FEATURETTE }
|
||||
?.sortedBy { it.type?.ordinal ?: 10000 }
|
||||
?.mapNotNull {
|
||||
when (it.site?.trim()?.lowercase()) {
|
||||
"youtube" -> { // TODO FILL SITES
|
||||
"https://www.youtube.com/watch?v=${it.key}"
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Movie.toLoadResponse(): MovieLoadResponse {
|
||||
return newMovieLoadResponse(
|
||||
this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink(
|
||||
this.imdb_id,
|
||||
this.id,
|
||||
null,
|
||||
null,
|
||||
this.title ?: this.original_title,
|
||||
).toJson()
|
||||
) {
|
||||
posterUrl = getImageUrl(poster_path)
|
||||
year = release_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
plot = overview
|
||||
addImdbId(external_ids?.imdb_id)
|
||||
tags = genres?.mapNotNull { it.name }
|
||||
duration = runtime
|
||||
rating = this@toLoadResponse.rating
|
||||
addTrailer(videos.toTrailers())
|
||||
|
||||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
addActors(credits?.cast?.toList().toActors())
|
||||
|
||||
contentRating = fetchContentRating(id, "US")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||
|
||||
// SAME AS DISCOVER IT SEEMS
|
||||
// val popularSeries = tmdb.tvService().popular(1, "en-US").execute().body()?.results?.map {
|
||||
// it.toSearchResponse()
|
||||
// } ?: listOf()
|
||||
//
|
||||
// val popularMovies =
|
||||
// tmdb.moviesService().popular(1, "en-US", "840").execute().body()?.results?.map {
|
||||
// it.toSearchResponse()
|
||||
// } ?: listOf()
|
||||
|
||||
var discoverMovies: List<MovieSearchResponse> = listOf()
|
||||
var discoverSeries: List<TvSeriesSearchResponse> = listOf()
|
||||
var topMovies: List<MovieSearchResponse> = listOf()
|
||||
var topSeries: List<TvSeriesSearchResponse> = listOf()
|
||||
argamap(
|
||||
{
|
||||
discoverMovies = tmdb.discoverMovie().build().awaitResponse().body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}, {
|
||||
discoverSeries = tmdb.discoverTv().build().awaitResponse().body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}, {
|
||||
// https://en.wikipedia.org/wiki/ISO_3166-1
|
||||
topMovies =
|
||||
tmdb.moviesService().topRated(1, "en-US", "US").awaitResponse()
|
||||
.body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}, {
|
||||
topSeries =
|
||||
tmdb.tvService().topRated(1, "en-US").awaitResponse().body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}
|
||||
)
|
||||
|
||||
return newHomePageResponse(
|
||||
listOf(
|
||||
// HomePageList("Popular Series", popularSeries),
|
||||
// HomePageList("Popular Movies", popularMovies),
|
||||
HomePageList("Popular Movies", discoverMovies),
|
||||
HomePageList("Popular Series", discoverSeries),
|
||||
HomePageList("Top Movies", topMovies),
|
||||
HomePageList("Top Series", topSeries),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
open fun loadFromImdb(imdb: String, seasons: List<TvSeason>): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun loadFromTmdb(tmdb: Int, seasons: List<TvSeason>): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun loadFromImdb(imdb: String): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun loadFromTmdb(tmdb: Int): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open suspend fun fetchContentRating(id: Int?, country: String): String? {
|
||||
id ?: return null
|
||||
|
||||
val contentRatings = tmdb.tvService().content_ratings(id).awaitResponse().body()?.results
|
||||
return if (!contentRatings.isNullOrEmpty()) {
|
||||
contentRatings.firstOrNull { it: ContentRating ->
|
||||
it.iso_3166_1 == country
|
||||
}?.rating
|
||||
} else {
|
||||
val releaseDates = tmdb.moviesService().releaseDates(id).awaitResponse().body()?.results
|
||||
val certification = releaseDates?.firstOrNull { it: ReleaseDatesResult ->
|
||||
it.iso_3166_1 == country
|
||||
}?.release_dates?.firstOrNull { it: ReleaseDate ->
|
||||
!it.certification.isNullOrBlank()
|
||||
}?.certification
|
||||
|
||||
certification
|
||||
}
|
||||
}
|
||||
|
||||
// Possible to add recommendations and such here.
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
// https://www.themoviedb.org/movie/7445-brothers
|
||||
// https://www.themoviedb.org/tv/71914-the-wheel-of-time
|
||||
|
||||
val idRegex = Regex("""themoviedb\.org/(.*)/(\d+)""")
|
||||
val found = idRegex.find(url)
|
||||
|
||||
val isTvSeries = found?.groupValues?.getOrNull(1).equals("tv", ignoreCase = true)
|
||||
val id = found?.groupValues?.getOrNull(2)?.toIntOrNull()
|
||||
?: throw ErrorLoadingException("No id found")
|
||||
|
||||
return if (useMetaLoadResponse) {
|
||||
return if (isTvSeries) {
|
||||
val body = tmdb.tvService()
|
||||
.tv(
|
||||
id,
|
||||
"en-US",
|
||||
AppendToResponse(
|
||||
AppendToResponseItem.EXTERNAL_IDS,
|
||||
AppendToResponseItem.VIDEOS
|
||||
)
|
||||
)
|
||||
.awaitResponse().body()
|
||||
val response = body?.toLoadResponse()
|
||||
if (response != null) {
|
||||
if (response.recommendations.isNullOrEmpty())
|
||||
tmdb.tvService().recommendations(id, 1, "en-US").awaitResponse().body()
|
||||
?.let {
|
||||
it.results?.map { res -> res.toSearchResponse() }
|
||||
}?.let { list ->
|
||||
response.recommendations = list
|
||||
}
|
||||
|
||||
if (response.actors.isNullOrEmpty())
|
||||
tmdb.tvService().credits(id, "en-US").awaitResponse().body()?.let {
|
||||
response.addActors(it.cast?.toActors())
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
} else {
|
||||
val body = tmdb.moviesService()
|
||||
.summary(
|
||||
id,
|
||||
"en-US",
|
||||
AppendToResponse(
|
||||
AppendToResponseItem.EXTERNAL_IDS,
|
||||
AppendToResponseItem.VIDEOS
|
||||
)
|
||||
)
|
||||
.awaitResponse().body()
|
||||
val response = body?.toLoadResponse()
|
||||
if (response != null) {
|
||||
if (response.recommendations.isNullOrEmpty())
|
||||
tmdb.moviesService().recommendations(id, 1, "en-US").awaitResponse().body()
|
||||
?.let {
|
||||
it.results?.map { res -> res.toSearchResponse() }
|
||||
}?.let { list ->
|
||||
response.recommendations = list
|
||||
}
|
||||
|
||||
if (response.actors.isNullOrEmpty())
|
||||
tmdb.moviesService().credits(id).awaitResponse().body()?.let {
|
||||
response.addActors(it.cast?.toActors())
|
||||
}
|
||||
}
|
||||
response
|
||||
}
|
||||
} else {
|
||||
loadFromTmdb(id)?.let { return it }
|
||||
if (isTvSeries) {
|
||||
tmdb.tvService().externalIds(id, "en-US").awaitResponse().body()?.imdb_id?.let {
|
||||
val fromImdb = loadFromImdb(it)
|
||||
val result = if (fromImdb == null) {
|
||||
val details = tmdb.tvService().tv(id, "en-US").awaitResponse().body()
|
||||
loadFromImdb(it, details?.seasons ?: listOf())
|
||||
?: loadFromTmdb(id, details?.seasons ?: listOf())
|
||||
} else fromImdb
|
||||
|
||||
result
|
||||
}
|
||||
} else {
|
||||
tmdb.moviesService().externalIds(id, "en-US").awaitResponse()
|
||||
.body()?.imdb_id?.let { loadFromImdb(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
return tmdb.searchService().multi(query, 1, "en-Us", "US", includeAdult).awaitResponse()
|
||||
.body()?.results?.mapNotNull {
|
||||
it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,470 @@
|
||||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.Actor
|
||||
import com.lagradost.cloudstream3.ActorData
|
||||
import com.lagradost.cloudstream3.Episode
|
||||
import com.lagradost.cloudstream3.HomePageResponse
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.MainPageRequest
|
||||
import com.lagradost.cloudstream3.NextAiring
|
||||
import com.lagradost.cloudstream3.ProviderType
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.ShowStatus
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.addDate
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.mainPageOf
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.newEpisode
|
||||
import com.lagradost.cloudstream3.newHomePageResponse
|
||||
import com.lagradost.cloudstream3.newMovieLoadResponse
|
||||
import com.lagradost.cloudstream3.newMovieSearchResponse
|
||||
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
|
||||
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import java.net.URI
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class TraktProvider : MainAPI() {
|
||||
override var name = "Trakt"
|
||||
override val hasMainPage = true
|
||||
override val providerType = ProviderType.MetaProvider
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
TvType.Anime,
|
||||
)
|
||||
|
||||
private val traktClientId =
|
||||
base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
|
||||
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
|
||||
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
|
||||
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
|
||||
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
|
||||
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
return newHomePageResponse(request.name, results)
|
||||
}
|
||||
|
||||
private fun MediaDetails.toSearchResponse(): SearchResponse {
|
||||
|
||||
val media = this.media ?: this
|
||||
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
|
||||
val poster = media.images?.poster?.firstOrNull()
|
||||
|
||||
if (mediaType == TvType.Movie) {
|
||||
return newMovieSearchResponse(
|
||||
name = media.title ?: "",
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.Movie,
|
||||
) {
|
||||
posterUrl = fixPath(poster)
|
||||
}
|
||||
} else {
|
||||
return newTvSeriesSearchResponse(
|
||||
name = media.title ?: "",
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.TvSeries,
|
||||
) {
|
||||
this.posterUrl = fixPath(poster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
val apiResponse =
|
||||
getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
|
||||
val data = parseJson<Data>(url)
|
||||
val mediaDetails = data.mediaDetails
|
||||
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
|
||||
|
||||
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
|
||||
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
|
||||
|
||||
val resActor =
|
||||
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
|
||||
|
||||
val actors = parseJson<People>(resActor).cast?.map {
|
||||
ActorData(
|
||||
Actor(
|
||||
name = it.person?.name!!,
|
||||
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
|
||||
),
|
||||
roleString = it.character
|
||||
)
|
||||
}
|
||||
|
||||
val resRelated =
|
||||
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
|
||||
|
||||
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
|
||||
|
||||
val isCartoon =
|
||||
mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
|
||||
val isAnime =
|
||||
isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
|
||||
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
|
||||
val isBollywood = mediaDetails?.country == "in"
|
||||
|
||||
if (data.type == TvType.Movie) {
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
airedDate = mediaDetails?.released
|
||||
?: mediaDetails?.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
).toJson()
|
||||
|
||||
return newMovieLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
dataUrl = linkData.toJson(),
|
||||
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
} else {
|
||||
|
||||
val resSeasons =
|
||||
getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
|
||||
val episodes = mutableListOf<Episode>()
|
||||
val seasons = parseJson<List<Seasons>>(resSeasons)
|
||||
var nextAir: NextAiring? = null
|
||||
|
||||
seasons.forEach { season ->
|
||||
|
||||
season.episodes?.map { episode ->
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
airedYear = mediaDetails?.year,
|
||||
lastSeason = seasons.size,
|
||||
epsTitle = episode.title,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
date = episode.firstAired,
|
||||
airedDate = episode.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
isCartoon = isCartoon
|
||||
).toJson()
|
||||
|
||||
episodes.add(
|
||||
newEpisode(linkData.toJson()) {
|
||||
this.name = episode.title
|
||||
this.season = episode.season
|
||||
this.episode = episode.number
|
||||
this.description = episode.overview
|
||||
this.runTime = episode.runtime
|
||||
this.posterUrl = fixPath(episode.images?.screenshot?.firstOrNull())
|
||||
this.rating = episode.rating?.times(10)?.roundToInt()
|
||||
|
||||
this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||
if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) {
|
||||
nextAir = NextAiring(
|
||||
episode = this.episode!!,
|
||||
unixTime = this.date!!.div(1000L),
|
||||
season = if (this.season == 1) null else this.season,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newTvSeriesLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
type = if (isAnime) TvType.Anime else TvType.TvSeries,
|
||||
episodes = episodes
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
||||
this.episodes = episodes
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.showStatus = getStatus(mediaDetails.status)
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.nextAiring = nextAir
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getApi(url: String): String {
|
||||
return app.get(
|
||||
url = url,
|
||||
headers = mapOf(
|
||||
"Content-Type" to "application/json",
|
||||
"trakt-api-version" to "2",
|
||||
"trakt-api-key" to traktClientId,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
private fun isUpcoming(dateString: String?): Boolean {
|
||||
return try {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
||||
unixTimeMS < dateTime
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(t: String?): ShowStatus {
|
||||
return when (t) {
|
||||
"returning series" -> ShowStatus.Ongoing
|
||||
"continuing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixPath(url: String?): String? {
|
||||
url ?: return null
|
||||
return "https://$url"
|
||||
}
|
||||
|
||||
private fun getWidthImageUrl(path: String?, width: String): String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
val fileName = URI(path).path?.substringAfterLast('/') ?: return null
|
||||
return "https://image.tmdb.org/t/p/${width}/${fileName}"
|
||||
}
|
||||
|
||||
private fun getOriginalWidthImageUrl(path: String?): String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
return getWidthImageUrl(path, "original")
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val type: TvType? = null,
|
||||
val mediaDetails: MediaDetails? = null,
|
||||
)
|
||||
|
||||
data class MediaDetails(
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("year") val year: Int? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("tagline") val tagline: String? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("released") val released: String? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("country") val country: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("trailer") val trailer: String? = null,
|
||||
@JsonProperty("homepage") val homepage: String? = null,
|
||||
@JsonProperty("status") val status: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("votes") val votes: Long? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Long? = null,
|
||||
@JsonProperty("language") val language: String? = null,
|
||||
@JsonProperty("languages") val languages: List<String>? = null,
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("genres") val genres: List<String>? = null,
|
||||
@JsonProperty("certification") val certification: String? = null,
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("airs") val airs: Airs? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
|
||||
)
|
||||
|
||||
data class Airs(
|
||||
@JsonProperty("day") val day: String? = null,
|
||||
@JsonProperty("time") val time: String? = null,
|
||||
@JsonProperty("timezone") val timezone: String? = null,
|
||||
)
|
||||
|
||||
data class Ids(
|
||||
@JsonProperty("trakt") val trakt: Int? = null,
|
||||
@JsonProperty("slug") val slug: String? = null,
|
||||
@JsonProperty("tvdb") val tvdb: Int? = null,
|
||||
@JsonProperty("imdb") val imdb: String? = null,
|
||||
@JsonProperty("tmdb") val tmdb: Int? = null,
|
||||
@JsonProperty("tvrage") val tvrage: String? = null,
|
||||
)
|
||||
|
||||
data class Images(
|
||||
@JsonProperty("fanart") val fanart: List<String>? = null,
|
||||
@JsonProperty("poster") val poster: List<String>? = null,
|
||||
@JsonProperty("logo") val logo: List<String>? = null,
|
||||
@JsonProperty("clearart") val clearart: List<String>? = null,
|
||||
@JsonProperty("banner") val banner: List<String>? = null,
|
||||
@JsonProperty("thumb") val thumb: List<String>? = null,
|
||||
@JsonProperty("screenshot") val screenshot: List<String>? = null,
|
||||
@JsonProperty("headshot") val headshot: List<String>? = null,
|
||||
)
|
||||
|
||||
data class People(
|
||||
@JsonProperty("cast") val cast: List<Cast>? = null,
|
||||
)
|
||||
|
||||
data class Cast(
|
||||
@JsonProperty("character") val character: String? = null,
|
||||
@JsonProperty("characters") val characters: List<String>? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Long? = null,
|
||||
@JsonProperty("person") val person: Person? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Person(
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Seasons(
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Int? = null,
|
||||
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class TraktEpisode(
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Int? = null,
|
||||
@JsonProperty("episode_type") val episodeType: String? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("number_abs") val numberAbs: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("season") val season: Int? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class LinkData(
|
||||
val id: Int? = null,
|
||||
val traktId: Int? = null,
|
||||
val traktSlug: String? = null,
|
||||
val tmdbId: Int? = null,
|
||||
val imdbId: String? = null,
|
||||
val tvdbId: Int? = null,
|
||||
val tvrageId: String? = null,
|
||||
val type: String? = null,
|
||||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
val aniId: String? = null,
|
||||
val animeId: String? = null,
|
||||
val title: String? = null,
|
||||
val year: Int? = null,
|
||||
val orgTitle: String? = null,
|
||||
val isAnime: Boolean = false,
|
||||
val airedYear: Int? = null,
|
||||
val lastSeason: Int? = null,
|
||||
val epsTitle: String? = null,
|
||||
val jpTitle: String? = null,
|
||||
val date: String? = null,
|
||||
val airedDate: String? = null,
|
||||
val isAsian: Boolean = false,
|
||||
val isBollywood: Boolean = false,
|
||||
val isCartoon: Boolean = false,
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user