package com.lagradost.cloudstream3.metaproviders import android.net.Uri import com.lagradost.cloudstream3.* import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty 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.mvvm.logError import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import java.util.Locale import java.text.SimpleDateFormat 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>(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? { val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") val results = parseJson>(apiResponse).map { element -> element.toSearchResponse() } return results } override suspend fun load(url: String): LoadResponse { val data = parseJson(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(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>(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, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, 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.apiName = "Trakt" 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() val seasons = parseJson>(resSeasons) val seasonsNames = mutableListOf() seasons.forEach { season -> seasonsNames.add( SeasonData( season.number!!, season.title ) ) season.episodes?.map { episode -> val linkData = LinkData( id = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, 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( Episode( data = linkData.toJson(), name = episode.title, season = episode.season, episode = episode.number, posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, ).apply { this.addDate(episode.firstAired) } ) } } return newTvSeriesLoadResponse( name = mediaDetails?.title!!, url = data.toJson(), type = if (isAnime) TvType.Anime else TvType.TvSeries, episodes = episodes ) { this.name = mediaDetails.title this.apiName = "Trakt" 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.seasonNames = seasonsNames 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 APIHolder.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.parse(path).lastPathSegment ?: 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? = null, @JsonProperty("available_translations") val availableTranslations: List? = null, @JsonProperty("genres") val genres: List? = 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? = null, @JsonProperty("poster") val poster: List? = null, @JsonProperty("logo") val logo: List? = null, @JsonProperty("clearart") val clearart: List? = null, @JsonProperty("banner") val banner: List? = null, @JsonProperty("thumb") val thumb: List? = null, @JsonProperty("screenshot") val screenshot: List? = null, @JsonProperty("headshot") val headshot: List? = null, ) data class People( @JsonProperty("cast") val cast: List? = null, ) data class Cast( @JsonProperty("character") val character: String? = null, @JsonProperty("characters") val characters: List? = 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? = 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? = 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 imdbId: String? = null, val tvdbId: Int? = 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, ) }