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:
Luna712
2025-02-16 10:46:48 -07:00
committed by GitHub
parent 157a02e6af
commit 0b94f76627
6 changed files with 13 additions and 7 deletions

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -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,
)
}