package com.streamflixreborn.streamflix.utils import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.annotations.SerializedName import com.streamflixreborn.streamflix.BuildConfig import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query import retrofit2.http.QueryMap import java.lang.reflect.Type import java.util.Calendar object TMDb3 { private const val URL = "https://api.themoviedb.org/3/" private var service = ApiService.build() fun rebuildService() { service = ApiService.build() } object Discover { suspend fun movie( certification: String? = null, certificationRange: Params.Range? = null, certificationCountry: String? = null, includeAdult: Boolean? = null, includeVideo: Boolean? = null, language: String? = null, page: Int? = null, primaryReleaseYear: Int? = null, primaryReleaseDate: Params.Range? = null, region: String? = null, releaseDate: Params.Range? = null, sortBy: Params.SortBy.Movie? = null, voteAverage: Params.Range? = null, voteCount: Params.Range? = null, watchRegion: String? = null, withCast: Params.WithBuilder? = null, withCompanies: Params.WithBuilder? = null, withCrew: Params.WithBuilder? = null, withGenres: Params.WithBuilder? = null, withKeywords: Params.WithBuilder? = null, withOriginCountry: Params.WithBuilder? = null, withOriginalLanguage: Params.WithBuilder? = null, withPeople: Params.WithBuilder? = null, withReleaseType: Params.WithBuilder? = null, withRuntime: Params.Range? = null, withWatchMonetizationTypes: Params.WithBuilder? = null, withWatchProviders: Params.WithBuilder? = null, withoutCompanies: Params.WithBuilder? = null, withoutGenres: Params.WithBuilder? = null, withoutKeywords: Params.WithBuilder? = null, withoutWatchProviders: Params.WithBuilder? = null, year: Int? = null, ): PageResult { val params = mapOf( Params.Key.CERTIFICATION to certification, Params.Key.CERTIFICATION_GTE to certificationRange?.gte, Params.Key.CERTIFICATION_LTE to certificationRange?.lte, Params.Key.CERTIFICATION_COUNTRY to certificationCountry, Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.INCLUDE_VIDEO to includeVideo?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), Params.Key.PRIMARY_RELEASE_YEAR to primaryReleaseYear?.toString(), Params.Key.PRIMARY_RELEASE_DATE_GTE to primaryReleaseDate?.gte?.format("yyyy-MM-dd"), Params.Key.PRIMARY_RELEASE_DATE_LTE to primaryReleaseDate?.lte?.format("yyyy-MM-dd"), Params.Key.REGION to region, Params.Key.RELEASE_DATE_GTE to releaseDate?.gte?.format("yyyy-MM-dd"), Params.Key.RELEASE_DATE_LTE to releaseDate?.lte?.format("yyyy-MM-dd"), Params.Key.SORT_BY to sortBy?.value, Params.Key.VOTE_AVERAGE_GTE to voteAverage?.gte?.toString(), Params.Key.VOTE_AVERAGE_LTE to voteAverage?.lte?.toString(), Params.Key.VOTE_COUNT_GTE to voteCount?.gte?.toString(), Params.Key.VOTE_COUNT_LTE to voteCount?.lte?.toString(), Params.Key.WATCH_REGION to watchRegion, Params.Key.WITH_CAST to withCast?.toString(), Params.Key.WITH_COMPANIES to withCompanies?.toString(), Params.Key.WITH_CREW to withCrew?.toString(), Params.Key.WITH_GENRES to withGenres?.toString(), Params.Key.WITH_KEYWORDS to withKeywords?.toString(), Params.Key.WITH_ORIGIN_COUNTRY to withOriginCountry?.toString(), Params.Key.WITH_ORIGINAL_LANGUAGE to withOriginalLanguage?.toString(), Params.Key.WITH_PEOPLE to withPeople?.toString(), Params.Key.WITH_RELEASE_TYPE to withReleaseType?.toString(), Params.Key.WITH_RUNTIME_GTE to withRuntime?.gte?.toString(), Params.Key.WITH_RUNTIME_LTE to withRuntime?.lte?.toString(), Params.Key.WITH_WATCH_MONETIZATION_TYPES to withWatchMonetizationTypes?.toString(), Params.Key.WITH_WATCH_PROVIDERS to withWatchProviders?.toString(), Params.Key.WITHOUT_COMPANIES to withoutCompanies?.toString(), Params.Key.WITHOUT_GENRES to withoutGenres?.toString(), Params.Key.WITHOUT_KEYWORDS to withoutKeywords?.toString(), Params.Key.WITHOUT_WATCH_PROVIDERS to withoutWatchProviders?.toString(), Params.Key.YEAR to year?.toString(), ) return service.getDiscoverMovies( params = params.filterNotNullValues(), ) } suspend fun movie(params: Map): PageResult { return service.getDiscoverMovies( params = params.filterNotNullValues(), ) } suspend fun tv( airDate: Params.Range? = null, firstAirDateYear: Int? = null, firstAirDate: Params.Range? = null, includeAdult: Boolean? = null, includeNullFirstAirDates: Boolean? = null, language: String? = null, page: Int? = null, screenedTheatrically: Boolean? = null, sortBy: Params.SortBy.Tv? = null, timezone: String? = null, voteAverage: Params.Range? = null, voteCount: Params.Range? = null, watchRegion: String? = null, withCompanies: Params.WithBuilder? = null, withGenres: Params.WithBuilder? = null, withKeywords: Params.WithBuilder? = null, withNetworks: Params.WithBuilder? = null, withOriginCountry: Params.WithBuilder? = null, withOriginalLanguage: Params.WithBuilder? = null, withRuntime: Params.Range? = null, withStatus: Params.WithBuilder? = null, withWatchMonetizationTypes: Params.WithBuilder? = null, withWatchProviders: Params.WithBuilder? = null, withoutCompanies: Params.WithBuilder? = null, withoutGenres: Params.WithBuilder? = null, withoutKeywords: Params.WithBuilder? = null, withoutWatchProviders: Params.WithBuilder? = null, withType: Params.WithBuilder? = null, ): PageResult { val params = mapOf( Params.Key.AIR_DATE_GTE to airDate?.gte?.format("yyyy-MM-dd"), Params.Key.AIR_DATE_LTE to airDate?.lte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_YEAR to firstAirDateYear?.toString(), Params.Key.FIRST_AIR_DATE_GTE to firstAirDate?.gte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_LTE to firstAirDate?.lte?.format("yyyy-MM-dd"), Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.INCLUDE_NULL_FIRST_AIR_DATES to includeNullFirstAirDates?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), Params.Key.SCREENED_THEATRICALLY to screenedTheatrically?.toString(), Params.Key.SORT_BY to sortBy?.value, Params.Key.TIMEZONE to timezone, Params.Key.VOTE_AVERAGE_GTE to voteAverage?.gte?.toString(), Params.Key.VOTE_AVERAGE_LTE to voteAverage?.lte?.toString(), Params.Key.VOTE_COUNT_GTE to voteCount?.gte?.toString(), Params.Key.VOTE_COUNT_LTE to voteCount?.lte?.toString(), Params.Key.WATCH_REGION to watchRegion, Params.Key.WITH_COMPANIES to withCompanies?.toString(), Params.Key.WITH_GENRES to withGenres?.toString(), Params.Key.WITH_KEYWORDS to withKeywords?.toString(), Params.Key.WITH_NETWORKS to withNetworks?.toString(), Params.Key.WITH_ORIGIN_COUNTRY to withOriginCountry?.toString(), Params.Key.WITH_ORIGINAL_LANGUAGE to withOriginalLanguage?.toString(), Params.Key.WITH_RUNTIME_GTE to withRuntime?.gte?.toString(), Params.Key.WITH_RUNTIME_LTE to withRuntime?.lte?.toString(), Params.Key.WITH_STATUS to withStatus?.toString(), Params.Key.WITH_WATCH_MONETIZATION_TYPES to withWatchMonetizationTypes?.toString(), Params.Key.WITH_WATCH_PROVIDERS to withWatchProviders?.toString(), Params.Key.WITHOUT_COMPANIES to withoutCompanies?.toString(), Params.Key.WITHOUT_GENRES to withoutGenres?.toString(), Params.Key.WITHOUT_KEYWORDS to withoutKeywords?.toString(), Params.Key.WITHOUT_WATCH_PROVIDERS to withoutWatchProviders?.toString(), Params.Key.WITH_TYPE to withType?.toString(), ) return service.getDiscoverTv( params = params.filterNotNullValues(), ) } suspend fun tv(params: Map): PageResult { return service.getDiscoverTv( params = params.filterNotNullValues(), ) } } object Genres { suspend fun movieList( language: String? = null, ): GenresResponse { val params = mapOf( Params.Key.LANGUAGE to language, ) return service.getGenreMoviesList( params = params.filterNotNullValues(), ) } suspend fun tvList( language: String? = null, ): GenresResponse { val params = mapOf( Params.Key.LANGUAGE to language, ) return service.getGenreTvList( params = params.filterNotNullValues(), ) } } object MovieLists { suspend fun popular( certification: String? = null, certificationRange: Params.Range? = null, certificationCountry: String? = null, includeAdult: Boolean? = null, includeVideo: Boolean? = null, language: String? = null, page: Int? = null, primaryReleaseYear: Int? = null, primaryReleaseDate: Params.Range? = null, region: String? = null, releaseDate: Params.Range? = null, sortBy: Params.SortBy.Movie? = null, voteAverage: Params.Range? = null, voteCount: Params.Range? = null, watchRegion: String? = null, withCast: Params.WithBuilder? = null, withCompanies: Params.WithBuilder? = null, withCrew: Params.WithBuilder? = null, withGenres: Params.WithBuilder? = null, withKeywords: Params.WithBuilder? = null, withOriginCountry: Params.WithBuilder? = null, withOriginalLanguage: Params.WithBuilder? = null, withPeople: Params.WithBuilder? = null, withReleaseType: Params.WithBuilder? = null, withRuntime: Params.Range? = null, withWatchMonetizationTypes: Params.WithBuilder? = null, withWatchProviders: Params.WithBuilder? = null, withoutCompanies: Params.WithBuilder? = null, withoutGenres: Params.WithBuilder? = null, withoutKeywords: Params.WithBuilder? = null, withoutWatchProviders: Params.WithBuilder? = null, year: Int? = null, ): PageResult { val params = mapOf( Params.Key.CERTIFICATION to certification, Params.Key.CERTIFICATION_GTE to certificationRange?.gte, Params.Key.CERTIFICATION_LTE to certificationRange?.lte, Params.Key.CERTIFICATION_COUNTRY to certificationCountry, Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.INCLUDE_VIDEO to includeVideo?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), Params.Key.PRIMARY_RELEASE_YEAR to primaryReleaseYear?.toString(), Params.Key.PRIMARY_RELEASE_DATE_GTE to primaryReleaseDate?.gte?.format("yyyy-MM-dd"), Params.Key.PRIMARY_RELEASE_DATE_LTE to primaryReleaseDate?.lte?.format("yyyy-MM-dd"), Params.Key.REGION to region, Params.Key.RELEASE_DATE_GTE to releaseDate?.gte?.format("yyyy-MM-dd"), Params.Key.RELEASE_DATE_LTE to releaseDate?.lte?.format("yyyy-MM-dd"), Params.Key.SORT_BY to sortBy?.value, Params.Key.VOTE_AVERAGE_GTE to voteAverage?.gte?.toString(), Params.Key.VOTE_AVERAGE_LTE to voteAverage?.lte?.toString(), Params.Key.VOTE_COUNT_GTE to voteCount?.gte?.toString(), Params.Key.VOTE_COUNT_LTE to voteCount?.lte?.toString(), Params.Key.WATCH_REGION to watchRegion, Params.Key.WITH_CAST to withCast?.toString(), Params.Key.WITH_COMPANIES to withCompanies?.toString(), Params.Key.WITH_CREW to withCrew?.toString(), Params.Key.WITH_GENRES to withGenres?.toString(), Params.Key.WITH_KEYWORDS to withKeywords?.toString(), Params.Key.WITH_ORIGIN_COUNTRY to withOriginCountry?.toString(), Params.Key.WITH_ORIGINAL_LANGUAGE to withOriginalLanguage?.toString(), Params.Key.WITH_PEOPLE to withPeople?.toString(), Params.Key.WITH_RELEASE_TYPE to withReleaseType?.toString(), Params.Key.WITH_RUNTIME_GTE to withRuntime?.gte?.toString(), Params.Key.WITH_RUNTIME_LTE to withRuntime?.lte?.toString(), Params.Key.WITH_WATCH_MONETIZATION_TYPES to withWatchMonetizationTypes?.toString(), Params.Key.WITH_WATCH_PROVIDERS to withWatchProviders?.toString(), Params.Key.WITHOUT_COMPANIES to withoutCompanies?.toString(), Params.Key.WITHOUT_GENRES to withoutGenres?.toString(), Params.Key.WITHOUT_KEYWORDS to withoutKeywords?.toString(), Params.Key.WITHOUT_WATCH_PROVIDERS to withoutWatchProviders?.toString(), Params.Key.YEAR to year?.toString(), ) return service.getPopularMovies( params = params.filterNotNullValues(), ) } suspend fun topRated( airDate: Params.Range? = null, firstAirDateYear: Int? = null, firstAirDate: Params.Range? = null, includeAdult: Boolean? = null, includeNullFirstAirDates: Boolean? = null, language: String? = null, page: Int? = null, screenedTheatrically: Boolean? = null, sortBy: Params.SortBy.Tv? = null, timezone: String? = null, voteAverage: Params.Range? = null, voteCount: Params.Range? = null, watchRegion: String? = null, withCompanies: Params.WithBuilder? = null, withGenres: Params.WithBuilder? = null, withKeywords: Params.WithBuilder? = null, withNetworks: Params.WithBuilder? = null, withOriginCountry: Params.WithBuilder? = null, withOriginalLanguage: Params.WithBuilder? = null, withRuntime: Params.Range? = null, withStatus: Params.WithBuilder? = null, withWatchMonetizationTypes: Params.WithBuilder? = null, withWatchProviders: Params.WithBuilder? = null, withoutCompanies: Params.WithBuilder? = null, withoutGenres: Params.WithBuilder? = null, withoutKeywords: Params.WithBuilder? = null, withoutWatchProviders: Params.WithBuilder? = null, withType: Params.WithBuilder? = null, ): PageResult { val params = mapOf( Params.Key.AIR_DATE_GTE to airDate?.gte?.format("yyyy-MM-dd"), Params.Key.AIR_DATE_LTE to airDate?.lte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_YEAR to firstAirDateYear?.toString(), Params.Key.FIRST_AIR_DATE_GTE to firstAirDate?.gte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_LTE to firstAirDate?.lte?.format("yyyy-MM-dd"), Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.INCLUDE_NULL_FIRST_AIR_DATES to includeNullFirstAirDates?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), Params.Key.SCREENED_THEATRICALLY to screenedTheatrically?.toString(), Params.Key.SORT_BY to sortBy?.value, Params.Key.TIMEZONE to timezone, Params.Key.VOTE_AVERAGE_GTE to voteAverage?.gte?.toString(), Params.Key.VOTE_AVERAGE_LTE to voteAverage?.lte?.toString(), Params.Key.VOTE_COUNT_GTE to voteCount?.gte?.toString(), Params.Key.VOTE_COUNT_LTE to voteCount?.lte?.toString(), Params.Key.WATCH_REGION to watchRegion, Params.Key.WITH_COMPANIES to withCompanies?.toString(), Params.Key.WITH_GENRES to withGenres?.toString(), Params.Key.WITH_KEYWORDS to withKeywords?.toString(), Params.Key.WITH_NETWORKS to withNetworks?.toString(), Params.Key.WITH_ORIGIN_COUNTRY to withOriginCountry?.toString(), Params.Key.WITH_ORIGINAL_LANGUAGE to withOriginalLanguage?.toString(), Params.Key.WITH_RUNTIME_GTE to withRuntime?.gte?.toString(), Params.Key.WITH_RUNTIME_LTE to withRuntime?.lte?.toString(), Params.Key.WITH_STATUS to withStatus?.toString(), Params.Key.WITH_WATCH_MONETIZATION_TYPES to withWatchMonetizationTypes?.toString(), Params.Key.WITH_WATCH_PROVIDERS to withWatchProviders?.toString(), Params.Key.WITHOUT_COMPANIES to withoutCompanies?.toString(), Params.Key.WITHOUT_GENRES to withoutGenres?.toString(), Params.Key.WITHOUT_KEYWORDS to withoutKeywords?.toString(), Params.Key.WITHOUT_WATCH_PROVIDERS to withoutWatchProviders?.toString(), Params.Key.WITH_TYPE to withType?.toString(), ) return service.getTopRatedMovies( params = params.filterNotNullValues(), ) } suspend fun topRated(params: Map): PageResult { return service.getTopRatedMovies( params = params.filterNotNullValues(), ) } } object Movies { suspend fun details( movieId: Int, appendToResponse: List? = null, language: String? = null, ): Movie.Detail { val params = mapOf( Params.Key.APPEND_TO_RESPONSE to appendToResponse?.joinToString(",") { it.value }, Params.Key.LANGUAGE to language, ) return service.getMovieDetails( movieId = movieId, params = params.filterNotNullValues(), ) } } object People { suspend fun details( personId: Int, appendToResponse: List? = null, language: String? = null, ): Person.Detail { val params = mapOf( Params.Key.APPEND_TO_RESPONSE to appendToResponse?.joinToString(",") { it.value }, Params.Key.LANGUAGE to language, ) return service.getPersonDetails( personId = personId, params = params.filterNotNullValues(), ) } } object Search { suspend fun multi( query: String, includeAdult: Boolean? = null, language: String? = null, page: Int? = null, ): PageResult { val params = mapOf( Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), ) return service.searchMulti( query = query, params = params.filterNotNullValues(), ) } } object Trending { suspend fun all( timeWindow: Params.TimeWindow, language: String? = null, page: Int? = null, ): PageResult { val params = mapOf( Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), ) return service.getTrendingAll( timeWindow = timeWindow.value, params = params.filterNotNullValues(), ) } } object TvSeriesLists { suspend fun airingToday( airDate: Params.Range? = null, firstAirDateYear: Int? = null, firstAirDate: Params.Range? = null, includeAdult: Boolean? = null, includeNullFirstAirDates: Boolean? = null, language: String? = null, page: Int? = null, screenedTheatrically: Boolean? = null, sortBy: Params.SortBy.Tv? = null, timezone: String? = null, voteAverage: Params.Range? = null, voteCount: Params.Range? = null, watchRegion: String? = null, withCompanies: Params.WithBuilder? = null, withGenres: Params.WithBuilder? = null, withKeywords: Params.WithBuilder? = null, withNetworks: Params.WithBuilder? = null, withOriginCountry: Params.WithBuilder? = null, withOriginalLanguage: Params.WithBuilder? = null, withRuntime: Params.Range? = null, withStatus: Params.WithBuilder? = null, withWatchMonetizationTypes: Params.WithBuilder? = null, withWatchProviders: Params.WithBuilder? = null, withoutCompanies: Params.WithBuilder? = null, withoutGenres: Params.WithBuilder? = null, withoutKeywords: Params.WithBuilder? = null, withoutWatchProviders: Params.WithBuilder? = null, withType: Params.WithBuilder? = null, ): PageResult { val params = mapOf( Params.Key.AIR_DATE_GTE to airDate?.gte?.format("yyyy-MM-dd"), Params.Key.AIR_DATE_LTE to airDate?.lte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_YEAR to firstAirDateYear?.toString(), Params.Key.FIRST_AIR_DATE_GTE to firstAirDate?.gte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_LTE to firstAirDate?.lte?.format("yyyy-MM-dd"), Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.INCLUDE_NULL_FIRST_AIR_DATES to includeNullFirstAirDates?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), Params.Key.SCREENED_THEATRICALLY to screenedTheatrically?.toString(), Params.Key.SORT_BY to sortBy?.value, Params.Key.TIMEZONE to timezone, Params.Key.VOTE_AVERAGE_GTE to voteAverage?.gte?.toString(), Params.Key.VOTE_AVERAGE_LTE to voteAverage?.lte?.toString(), Params.Key.VOTE_COUNT_GTE to voteCount?.gte?.toString(), Params.Key.VOTE_COUNT_LTE to voteCount?.lte?.toString(), Params.Key.WATCH_REGION to watchRegion, Params.Key.WITH_COMPANIES to withCompanies?.toString(), Params.Key.WITH_GENRES to withGenres?.toString(), Params.Key.WITH_KEYWORDS to withKeywords?.toString(), Params.Key.WITH_NETWORKS to withNetworks?.toString(), Params.Key.WITH_ORIGIN_COUNTRY to withOriginCountry?.toString(), Params.Key.WITH_ORIGINAL_LANGUAGE to withOriginalLanguage?.toString(), Params.Key.WITH_RUNTIME_GTE to withRuntime?.gte?.toString(), Params.Key.WITH_RUNTIME_LTE to withRuntime?.lte?.toString(), Params.Key.WITH_STATUS to withStatus?.toString(), Params.Key.WITH_WATCH_MONETIZATION_TYPES to withWatchMonetizationTypes?.toString(), Params.Key.WITH_WATCH_PROVIDERS to withWatchProviders?.toString(), Params.Key.WITHOUT_COMPANIES to withoutCompanies?.toString(), Params.Key.WITHOUT_GENRES to withoutGenres?.toString(), Params.Key.WITHOUT_KEYWORDS to withoutKeywords?.toString(), Params.Key.WITHOUT_WATCH_PROVIDERS to withoutWatchProviders?.toString(), Params.Key.WITH_TYPE to withType?.toString(), ) return service.getAiringTodayTv( params = params.filterNotNullValues(), ) } suspend fun popular( airDate: Params.Range? = null, firstAirDateYear: Int? = null, firstAirDate: Params.Range? = null, includeAdult: Boolean? = null, includeNullFirstAirDates: Boolean? = null, language: String? = null, page: Int? = null, screenedTheatrically: Boolean? = null, sortBy: Params.SortBy.Tv? = null, timezone: String? = null, voteAverage: Params.Range? = null, voteCount: Params.Range? = null, watchRegion: String? = null, withCompanies: Params.WithBuilder? = null, withGenres: Params.WithBuilder? = null, withKeywords: Params.WithBuilder? = null, withNetworks: Params.WithBuilder? = null, withOriginCountry: Params.WithBuilder? = null, withOriginalLanguage: Params.WithBuilder? = null, withRuntime: Params.Range? = null, withStatus: Params.WithBuilder? = null, withWatchMonetizationTypes: Params.WithBuilder? = null, withWatchProviders: Params.WithBuilder? = null, withoutCompanies: Params.WithBuilder? = null, withoutGenres: Params.WithBuilder? = null, withoutKeywords: Params.WithBuilder? = null, withoutWatchProviders: Params.WithBuilder? = null, withType: Params.WithBuilder? = null, ): PageResult { val params = mapOf( Params.Key.AIR_DATE_GTE to airDate?.gte?.format("yyyy-MM-dd"), Params.Key.AIR_DATE_LTE to airDate?.lte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_YEAR to firstAirDateYear?.toString(), Params.Key.FIRST_AIR_DATE_GTE to firstAirDate?.gte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_LTE to firstAirDate?.lte?.format("yyyy-MM-dd"), Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.INCLUDE_NULL_FIRST_AIR_DATES to includeNullFirstAirDates?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), Params.Key.SCREENED_THEATRICALLY to screenedTheatrically?.toString(), Params.Key.SORT_BY to sortBy?.value, Params.Key.TIMEZONE to timezone, Params.Key.VOTE_AVERAGE_GTE to voteAverage?.gte?.toString(), Params.Key.VOTE_AVERAGE_LTE to voteAverage?.lte?.toString(), Params.Key.VOTE_COUNT_GTE to voteCount?.gte?.toString(), Params.Key.VOTE_COUNT_LTE to voteCount?.lte?.toString(), Params.Key.WATCH_REGION to watchRegion, Params.Key.WITH_COMPANIES to withCompanies?.toString(), Params.Key.WITH_GENRES to withGenres?.toString(), Params.Key.WITH_KEYWORDS to withKeywords?.toString(), Params.Key.WITH_NETWORKS to withNetworks?.toString(), Params.Key.WITH_ORIGIN_COUNTRY to withOriginCountry?.toString(), Params.Key.WITH_ORIGINAL_LANGUAGE to withOriginalLanguage?.toString(), Params.Key.WITH_RUNTIME_GTE to withRuntime?.gte?.toString(), Params.Key.WITH_RUNTIME_LTE to withRuntime?.lte?.toString(), Params.Key.WITH_STATUS to withStatus?.toString(), Params.Key.WITH_WATCH_MONETIZATION_TYPES to withWatchMonetizationTypes?.toString(), Params.Key.WITH_WATCH_PROVIDERS to withWatchProviders?.toString(), Params.Key.WITHOUT_COMPANIES to withoutCompanies?.toString(), Params.Key.WITHOUT_GENRES to withoutGenres?.toString(), Params.Key.WITHOUT_KEYWORDS to withoutKeywords?.toString(), Params.Key.WITHOUT_WATCH_PROVIDERS to withoutWatchProviders?.toString(), Params.Key.WITH_TYPE to withType?.toString(), ) return service.getPopularTv( params = params.filterNotNullValues(), ) } suspend fun topRated( airDate: Params.Range? = null, firstAirDateYear: Int? = null, firstAirDate: Params.Range? = null, includeAdult: Boolean? = null, includeNullFirstAirDates: Boolean? = null, language: String? = null, page: Int? = null, screenedTheatrically: Boolean? = null, sortBy: Params.SortBy.Tv? = null, timezone: String? = null, voteAverage: Params.Range? = null, voteCount: Params.Range? = null, watchRegion: String? = null, withCompanies: Params.WithBuilder? = null, withGenres: Params.WithBuilder? = null, withKeywords: Params.WithBuilder? = null, withNetworks: Params.WithBuilder? = null, withOriginCountry: Params.WithBuilder? = null, withOriginalLanguage: Params.WithBuilder? = null, withRuntime: Params.Range? = null, withStatus: Params.WithBuilder? = null, withWatchMonetizationTypes: Params.WithBuilder? = null, withWatchProviders: Params.WithBuilder? = null, withoutCompanies: Params.WithBuilder? = null, withoutGenres: Params.WithBuilder? = null, withoutKeywords: Params.WithBuilder? = null, withoutWatchProviders: Params.WithBuilder? = null, withType: Params.WithBuilder? = null, ): PageResult { val params = mapOf( Params.Key.AIR_DATE_GTE to airDate?.gte?.format("yyyy-MM-dd"), Params.Key.AIR_DATE_LTE to airDate?.lte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_YEAR to firstAirDateYear?.toString(), Params.Key.FIRST_AIR_DATE_GTE to firstAirDate?.gte?.format("yyyy-MM-dd"), Params.Key.FIRST_AIR_DATE_LTE to firstAirDate?.lte?.format("yyyy-MM-dd"), Params.Key.INCLUDE_ADULT to includeAdult?.toString(), Params.Key.INCLUDE_NULL_FIRST_AIR_DATES to includeNullFirstAirDates?.toString(), Params.Key.LANGUAGE to language, Params.Key.PAGE to page?.toString(), Params.Key.SCREENED_THEATRICALLY to screenedTheatrically?.toString(), Params.Key.SORT_BY to sortBy?.value, Params.Key.TIMEZONE to timezone, Params.Key.VOTE_AVERAGE_GTE to voteAverage?.gte?.toString(), Params.Key.VOTE_AVERAGE_LTE to voteAverage?.lte?.toString(), Params.Key.VOTE_COUNT_GTE to voteCount?.gte?.toString(), Params.Key.VOTE_COUNT_LTE to voteCount?.lte?.toString(), Params.Key.WATCH_REGION to watchRegion, Params.Key.WITH_COMPANIES to withCompanies?.toString(), Params.Key.WITH_GENRES to withGenres?.toString(), Params.Key.WITH_KEYWORDS to withKeywords?.toString(), Params.Key.WITH_NETWORKS to withNetworks?.toString(), Params.Key.WITH_ORIGIN_COUNTRY to withOriginCountry?.toString(), Params.Key.WITH_ORIGINAL_LANGUAGE to withOriginalLanguage?.toString(), Params.Key.WITH_RUNTIME_GTE to withRuntime?.gte?.toString(), Params.Key.WITH_RUNTIME_LTE to withRuntime?.lte?.toString(), Params.Key.WITH_STATUS to withStatus?.toString(), Params.Key.WITH_WATCH_MONETIZATION_TYPES to withWatchMonetizationTypes?.toString(), Params.Key.WITH_WATCH_PROVIDERS to withWatchProviders?.toString(), Params.Key.WITHOUT_COMPANIES to withoutCompanies?.toString(), Params.Key.WITHOUT_GENRES to withoutGenres?.toString(), Params.Key.WITHOUT_KEYWORDS to withoutKeywords?.toString(), Params.Key.WITHOUT_WATCH_PROVIDERS to withoutWatchProviders?.toString(), Params.Key.WITH_TYPE to withType?.toString(), ) return service.getTopRatedTv( params = params.filterNotNullValues(), ) } suspend fun topRated(params: Map): PageResult { return service.getTopRatedTv( params = params.filterNotNullValues(), ) } } object TvSeries { suspend fun details( seriesId: Int, appendToResponse: List? = null, language: String? = null, ): Tv.Detail { val params = mapOf( Params.Key.APPEND_TO_RESPONSE to appendToResponse?.joinToString(",") { it.value }, Params.Key.LANGUAGE to language, ) return service.getTvDetails( seriesId = seriesId, params = params.filterNotNullValues(), ) } } object TvSeasons { suspend fun details( seriesId: Int, seasonNumber: Int, appendToResponse: List? = null, language: String? = null, ): Season.Detail { val params = mapOf( Params.Key.APPEND_TO_RESPONSE to appendToResponse?.joinToString(",") { it.value }, Params.Key.LANGUAGE to language, ) return service.getTvSeasonDetails( seriesId = seriesId, seasonNumber = seasonNumber, params = params.filterNotNullValues(), ) } } object Params { data class Range( val gte: T? = null, val lte: T? = null, ) object SortBy { enum class Movie(val value: String) { ORIGINAL_TITLE_ASC("original_title.asc"), ORIGINAL_TITLE_DESC("original_title.desc"), POPULARITY_ASC("popularity.asc"), POPULARITY_DESC("popularity.desc"), PRIMARY_RELEASE_DATE_ASC("primary_release_date.asc"), PRIMARY_RELEASE_DATE_DESC("primary_release_date.desc"), RELEASE_DATE_ASC("release_date.asc"), RELEASE_DATE_DESC("release_date.desc"), REVENUE_ASC("revenue.asc"), REVENUE_DESC("revenue.desc"), TITLE_ASC("title.asc"), TITLE_DESC("title.desc"), VOTE_AVERAGE_ASC("vote_average.asc"), VOTE_AVERAGE_DESC("vote_average.desc"), VOTE_COUNT_ASC("vote_count.asc"), VOTE_COUNT_DESC("vote_count.desc"), } enum class Tv(val value: String) { FIRST_AIR_DATE_ASC("first_air_date.asc"), FIRST_AIR_DATE_DESC("first_air_date.desc"), NAME_ASC("name.asc"), NAME_DESC("name.desc"), ORIGINAL_NAME_ASC("original_name.asc"), ORIGINAL_NAME_DESC("original_name.desc"), POPULARITY_ASC("popularity.asc"), POPULARITY_DESC("popularity.desc"), VOTE_AVERAGE_ASC("vote_average.asc"), VOTE_AVERAGE_DESC("vote_average.desc"), VOTE_COUNT_ASC("vote_count.asc"), VOTE_COUNT_DESC("vote_count.desc"), } } object AppendToResponse { enum class Movie(val value: String) { ACCOUNT_STATES("account_states"), ALTERNATIVE_TITLES("alternative_titles"), CHANGES("changes"), CREDITS("credits"), EXTERNAL_IDS("external_ids"), IMAGES("images"), KEYWORDS("keywords"), LISTS("lists"), RECOMMENDATIONS("recommendations"), RELEASES_DATES("release_dates"), REVIEWS("reviews"), SIMILAR("similar"), TRANSLATIONS("translations"), VIDEOS("videos"), WATCH_PROVIDERS("watch/providers"), } enum class Person(val value: String) { CHANGES("changes"), COMBINED_CREDITS("combined_credits"), EXTERNAL_IDS("external_ids"), IMAGES("images"), MOVIE_CREDITS("movie_credits"), TV_CREDITS("tv_credits"), TRANSLATIONS("translations"), @Deprecated("This method is deprecated") TAGGED_IMAGES("tagged_images"), } enum class Tv(val value: String) { ACCOUNT_STATES("account_states"), AGGREGATE_CREDITS("aggregate_credits"), ALTERNATIVE_TITLES("alternative_titles"), CHANGES("changes"), CONTENT_RATING("content_ratings"), CREDITS("credits"), EPISODE_GROUPS("episode_groups"), EXTERNAL_IDS("external_ids"), IMAGES("images"), KEYWORDS("keywords"), LISTS("lists"), RECOMMENDATIONS("recommendations"), REVIEWS("reviews"), SCREENED_THEATRICALLY("screened_theatrically"), SIMILAR("similar"), TRANSLATIONS("translations"), VIDEOS("videos"), WATCH_PROVIDERS("watch/providers"), } enum class TvSeason(val value: String) { ACCOUNT_STATES("account_states"), AGGREGATE_CREDITS("aggregate_credits"), CHANGES("changes"), CREDITS("credits"), EXTERNAL_IDS("external_ids"), IMAGES("images"), TRANSLATIONS("translations"), VIDEOS("videos"), WATCH_PROVIDERS("watch/providers"), } } enum class TimeWindow(val value: String) { DAY("day"), WEEK("week"); } object Key { const val AIR_DATE_GTE = "air_date.gte" const val AIR_DATE_LTE = "air_date.lte" const val APPEND_TO_RESPONSE = "append_to_response" const val CERTIFICATION = "certification" const val CERTIFICATION_COUNTRY = "certification_country" const val CERTIFICATION_GTE = "certification.gte" const val CERTIFICATION_LTE = "certification.lte" const val FIRST_AIR_DATE_GTE = "first_air_date.gte" const val FIRST_AIR_DATE_LTE = "first_air_date.lte" const val FIRST_AIR_DATE_YEAR = "first_air_date_year" const val INCLUDE_ADULT = "include_adult" const val INCLUDE_NULL_FIRST_AIR_DATES = "include_null_first_air_dates" const val INCLUDE_VIDEO = "include_video" const val LANGUAGE = "language" const val PAGE = "page" const val PRIMARY_RELEASE_DATE_GTE = "primary_release_date.gte" const val PRIMARY_RELEASE_DATE_LTE = "primary_release_date.lte" const val PRIMARY_RELEASE_YEAR = "primary_release_year" const val REGION = "region" const val RELEASE_DATE_GTE = "release_date.gte" const val RELEASE_DATE_LTE = "release_date.lte" const val SCREENED_THEATRICALLY = "screened_theatrically" const val SORT_BY = "sort_by" const val TIMEZONE = "timezone" const val VOTE_AVERAGE_GTE = "vote_average.gte" const val VOTE_AVERAGE_LTE = "vote_average.lte" const val VOTE_COUNT_GTE = "vote_count.gte" const val VOTE_COUNT_LTE = "vote_count.lte" const val WATCH_REGION = "watch_region" const val WITHOUT_COMPANIES = "without_companies" const val WITHOUT_GENRES = "without_genres" const val WITHOUT_KEYWORDS = "without_keywords" const val WITHOUT_WATCH_PROVIDERS = "without_watch_providers" const val WITH_CAST = "with_cast" const val WITH_COMPANIES = "with_companies" const val WITH_CREW = "with_crew" const val WITH_GENRES = "with_genres" const val WITH_KEYWORDS = "with_keywords" const val WITH_NETWORKS = "with_networks" const val WITH_ORIGINAL_LANGUAGE = "with_original_language" const val WITH_ORIGIN_COUNTRY = "with_origin_country" const val WITH_PEOPLE = "with_people" const val WITH_RELEASE_TYPE = "with_release_type" const val WITH_RUNTIME_GTE = "with_runtime.gte" const val WITH_RUNTIME_LTE = "with_runtime.lte" const val WITH_STATUS = "with_status" const val WITH_TYPE = "with_type" const val WITH_WATCH_MONETIZATION_TYPES = "with_watch_monetization_types" const val WITH_WATCH_PROVIDERS = "with_watch_providers" const val YEAR = "year" } class WithBuilder { private var value: String = "" constructor(id: Int) { value += id } constructor(id: String) { value += id } constructor(any: T) { value += any.toString() } fun and(id: Int): WithBuilder { value += ",$id" return this } fun and(id: String): WithBuilder { value += ",$id" return this } fun and(any: T): WithBuilder { value += ",$any" return this } fun or(id: Int): WithBuilder { value += "|$id" return this } fun or(id: String): WithBuilder { value += "|$id" return this } fun or(any: T): WithBuilder { value += "|$any" return this } override fun toString() = value } } val String.w500: String get() = when (this.startsWith("/")) { true -> "https://image.tmdb.org/t/p/w500/$this" else -> this } val String.original: String get() = when (this.startsWith("/")) { true -> "https://image.tmdb.org/t/p/original/$this" else -> this } private interface ApiService { companion object { fun build(): ApiService { val apiKey = UserPreferences.tmdbApiKey.ifEmpty { BuildConfig.TMDB_API_KEY } val client = OkHttpClient.Builder().addInterceptor { chain -> val original = chain.request() val requestBuilder = original.newBuilder() .url( original.url.newBuilder() .addQueryParameter("api_key", apiKey) .build() ) chain.proceed(requestBuilder.build()) }.build() val retrofit = Retrofit.Builder() .baseUrl(URL) .client(client) .addConverterFactory( GsonConverterFactory.create( GsonBuilder() .registerTypeAdapter( MultiItem::class.java, MultiItem.Deserializer() ) .create() ) ) .build() return retrofit.create(ApiService::class.java) } } @GET("discover/movie") suspend fun getDiscoverMovies( @QueryMap params: Map = emptyMap(), ): PageResult @GET("discover/tv") suspend fun getDiscoverTv( @QueryMap params: Map = emptyMap(), ): PageResult @GET("genre/movie/list") suspend fun getGenreMoviesList( @QueryMap params: Map = emptyMap(), ): GenresResponse @GET("genre/tv/list") suspend fun getGenreTvList( @QueryMap params: Map = emptyMap(), ): GenresResponse @GET("movie/popular") suspend fun getPopularMovies( @QueryMap params: Map = emptyMap(), ): PageResult @GET("movie/top_rated") suspend fun getTopRatedMovies( @QueryMap params: Map = emptyMap(), ): PageResult @GET("movie/{movie_id}") suspend fun getMovieDetails( @Path("movie_id") movieId: Int, @QueryMap params: Map = emptyMap(), ): Movie.Detail @GET("person/{person_id}") suspend fun getPersonDetails( @Path("person_id") personId: Int, @QueryMap params: Map = emptyMap(), ): Person.Detail @GET("search/multi") suspend fun searchMulti( @Query("query") query: String, @QueryMap params: Map = emptyMap(), ): PageResult @GET("trending/all/{time_window}") suspend fun getTrendingAll( @Path("time_window") timeWindow: String, @QueryMap params: Map = emptyMap(), ): PageResult @GET("tv/airing_today") suspend fun getAiringTodayTv( @QueryMap params: Map = emptyMap(), ): PageResult @GET("tv/popular") suspend fun getPopularTv( @QueryMap params: Map = emptyMap(), ): PageResult @GET("tv/top_rated") suspend fun getTopRatedTv( @QueryMap params: Map = emptyMap(), ): PageResult @GET("tv/{series_id}") suspend fun getTvDetails( @Path("series_id") seriesId: Int, @QueryMap params: Map = emptyMap(), ): Tv.Detail @GET("tv/{series_id}/season/{season_number}") suspend fun getTvSeasonDetails( @Path("series_id") seriesId: Int, @Path("season_number") seasonNumber: Int, @QueryMap params: Map = emptyMap(), ): Season.Detail } data class Result( val results: List ) data class PageResult( @SerializedName("page") val page: Int, @SerializedName("results") val results: List = emptyList(), @SerializedName("total_pages") val totalPages: Int, @SerializedName("total_results") val totalResults: Int, ) data class GenresResponse( val genres: List, ) sealed class MultiItem { enum class MediaType { @SerializedName("movie") MOVIE, @SerializedName("person") PERSON, @SerializedName("tv") TV, } class Deserializer : JsonDeserializer { override fun deserialize( json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext? ): MultiItem? { val jsonObject = json?.asJsonObject ?: JsonObject() return when (jsonObject.get("media_type")?.asString ?: "") { "movie" -> Gson().fromJson(json, Movie::class.java) "person" -> Gson().fromJson(json, Person::class.java) "tv" -> Gson().fromJson(json, Tv::class.java) else -> null } } } } data class WatchProviderResult( val id: Int? = null, val results: Map ) data class Genre( val id: Int, val name: String, ) { enum class Movie(val id: Int) { ACTION(28), ADVENTURE(12), ANIMATION(16), COMEDY(35), CRIME(80), DOCUMENTARY(99), DRAMA(18), FAMILY(10751), FANTASY(14), HISTORY(36), HORROR(27), MUSIC(10402), MYSTERY(9648), ROMANCE(10749), SCIENCE_FICTION(878), TV_MOVIE(10770), THRILLER(53), WAR(10752), WESTERN(37); override fun toString() = id.toString() } enum class Tv(val id: Int) { ACTION_ADVENTURE(10759), ANIMATION(16), COMEDY(35), CRIME(80), DOCUMENTARY(99), DRAMA(18), FAMILY(10751), KIDS(10762), MYSTERY(9648), NEWS(10763), REALITY(10764), SCIENCE_FICTION_FANTASY(10765), SOAP(10766), TALK(10767), WAR_POLITICS(10768), WESTERN(37); override fun toString() = id.toString() } } data class Movie( @SerializedName("poster_path") val posterPath: String?, @SerializedName("adult") val adult: Boolean = false, @SerializedName("overview") val overview: String, @SerializedName("release_date") val releaseDate: String? = null, @SerializedName("genre_ids") val genresIds: List, @SerializedName("id") val id: Int, @SerializedName("original_title") val originalTitle: String, @SerializedName("original_language") val originalLanguage: String, @SerializedName("title") val title: String, @SerializedName("backdrop_path") val backdropPath: String?, @SerializedName("popularity") val popularity: Float, @SerializedName("vote_count") val voteCount: Int, @SerializedName("video") val video: Boolean, @SerializedName("vote_average") val voteAverage: Float, ) : MultiItem() { enum class ReleaseType(val value: Int) { @SerializedName("1") PREMIERE(1), @SerializedName("2") THEATRICAL_LIMITED(2), @SerializedName("3") THEATRICAL(3), @SerializedName("4") DIGITAL(4), @SerializedName("5") PHYSICAL(5), @SerializedName("6") TV(6), } enum class Status(val value: String) { @SerializedName("Rumored") RUMORED("Rumored"), @SerializedName("Planned") PLANNED("Planned"), @SerializedName("In Production") IN_PRODUCTION("In Production"), @SerializedName("Post Production") POST_PRODUCTION("Post Production"), @SerializedName("Released") RELEASED("Released"), @SerializedName("Canceled") CANCELED("Canceled"), } data class Detail( val adult: Boolean, @SerializedName("backdrop_path") val backdropPath: String?, val budget: Long, @SerializedName("genres") val genres: List, val homepage: String? = null, val id: Int, @SerializedName("imdb_id") val imdbId: String? = null, val title: String, val runtime: Int? = null, @SerializedName("original_title") val originalTitle: String, @SerializedName("original_language") val originalLanguage: String, val overview: String, @SerializedName("poster_path") val posterPath: String?, @SerializedName("vote_average") val voteAverage: Float, @SerializedName("vote_count") val voteCount: Int, @SerializedName("external_ids") val externalIds: ExternalIds? = null, val status: Status, val tagline: String, val video: Boolean, val popularity: Float, @SerializedName("release_date") val releaseDate: String?, val revenue: Long, @SerializedName("release_dates") val releaseDates: Result? = null, @SerializedName("production_companies") val productionCompanies: List? = null, @SerializedName("production_countries") val productionCountries: List? = null, @SerializedName("watch/providers") val watchProviders: WatchProviderResult? = null, @SerializedName("credits") val credits: Credits? = null, @SerializedName("videos") val videos: Result