move TMDBApiCOntroller to Fuel and kotlinx.serialization
* add year and maturityRatings to MediaFragment * don't show season selection if only one season is present
This commit is contained in:
		| @ -76,6 +76,8 @@ object Crunchyroll { | ||||
|     ): Result<FuelJson, FuelError> = coroutineScope { | ||||
|         val path = if (url.isEmpty()) "$baseUrl$endpoint" else url | ||||
|  | ||||
|         // TODO before sending a request, make sure the accessToken is not expired | ||||
|  | ||||
|         return@coroutineScope (Dispatchers.IO) { | ||||
|             val (request, response, result) = Fuel.get(path, params) | ||||
|                 .header("Authorization", "$tokenType $accessToken") | ||||
| @ -182,7 +184,6 @@ object Crunchyroll { | ||||
|         val result = request(episodesEndpoint, parameters) | ||||
|  | ||||
|         return result.component1()?.obj()?.let { | ||||
|             println(it) | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneSeasons | ||||
|     } | ||||
| @ -200,7 +201,6 @@ object Crunchyroll { | ||||
|         val result = request(episodesEndpoint, parameters) | ||||
|  | ||||
|         return result.component1()?.obj()?.let { | ||||
|             println(it) | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneEpisodes | ||||
|     } | ||||
| @ -209,7 +209,6 @@ object Crunchyroll { | ||||
|         val result = request("", url = url) | ||||
|  | ||||
|         return result.component1()?.obj()?.let { | ||||
|             println(it) | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NonePlayback | ||||
|     } | ||||
|  | ||||
| @ -64,12 +64,13 @@ data class Poster(val height: Int, val width: Int, val source: String, val type: | ||||
|  */ | ||||
| @Serializable | ||||
| data class Series( | ||||
|     val id: String, | ||||
|     val title: String, | ||||
|     val description: String, | ||||
|     val images: Images | ||||
|     @SerialName("id") val id: String, | ||||
|     @SerialName("title") val title: String, | ||||
|     @SerialName("description") val description: String, | ||||
|     @SerialName("images") val images: Images, | ||||
|     @SerialName("maturity_ratings") val maturityRatings: List<String> | ||||
| ) | ||||
| val NoneSeries = Series("", "", "", Images(listOf(), listOf())) | ||||
| val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList()) | ||||
|  | ||||
|  | ||||
| /** | ||||
| @ -77,8 +78,8 @@ val NoneSeries = Series("", "", "", Images(listOf(), listOf())) | ||||
|  */ | ||||
| @Serializable | ||||
| data class Seasons( | ||||
|     val total: Int, | ||||
|     val items: List<Season> | ||||
|     @SerialName("total") val total: Int, | ||||
|     @SerialName("items") val items: List<Season> | ||||
| ) { | ||||
|     fun getPreferredSeason(local: Locale): Season { | ||||
|         // try to get the the first seasons which matches the preferred local | ||||
| @ -111,14 +112,17 @@ data class Season( | ||||
| ) | ||||
|  | ||||
| val NoneSeasons = Seasons(0, listOf()) | ||||
| val NoneSeason = Season("", "", "", 0, false, false) | ||||
| val NoneSeason = Season("", "", "", 0, isSubbed = false, isDubbed = false) | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Episodes data type | ||||
|  */ | ||||
| @Serializable | ||||
| data class Episodes(val total: Int, val items: List<Episode>) | ||||
| data class Episodes( | ||||
|     @SerialName("total") val total: Int, | ||||
|     @SerialName("items") val items: List<Episode> | ||||
| ) | ||||
|  | ||||
| @Serializable | ||||
| data class Episode( | ||||
|  | ||||
| @ -24,10 +24,9 @@ import org.mosad.teapod.parser.crunchyroll.Item | ||||
| import org.mosad.teapod.parser.crunchyroll.NoneItem | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
| import org.mosad.teapod.util.StorageController | ||||
| import org.mosad.teapod.util.tmdb.TMDBMovie | ||||
| import org.mosad.teapod.util.tmdb.TMDBApiController | ||||
| import org.mosad.teapod.util.tmdb.TMDBMovie | ||||
| import org.mosad.teapod.util.tmdb.TMDBTVShow | ||||
|  | ||||
| /** | ||||
|  * The media detail fragment. | ||||
| @ -48,6 +47,7 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|  | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         binding.frameLoading.visibility = View.VISIBLE | ||||
| @ -89,9 +89,9 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : | ||||
|      */ | ||||
|     private fun updateGUI() = with(model) { | ||||
|         // generic gui | ||||
|         val backdropUrl = tmdbResult?.backdropPath?.let { TMDBApiController.imageUrl + it } | ||||
|         val backdropUrl = tmdbResult.backdropPath?.let { TMDBApiController.imageUrl + it } | ||||
|             ?: seriesCrunchy.images.poster_wide[0][2].source | ||||
|         val posterUrl = tmdbResult?.posterPath?.let { TMDBApiController.imageUrl + it } | ||||
|         val posterUrl = tmdbResult.posterPath?.let { TMDBApiController.imageUrl + it } | ||||
|             ?: seriesCrunchy.images.poster_tall[0][2].source | ||||
|  | ||||
|         // load poster and backdrop | ||||
| @ -103,9 +103,14 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : | ||||
|             .into(binding.imageBackdrop) | ||||
|  | ||||
|         binding.textTitle.text = seriesCrunchy.title | ||||
|         //binding.textYear.text = media.year.toString() // TODO get from tmdb | ||||
|         //binding.textAge.text = media.age.toString() // TODO get from tmdb | ||||
|         binding.textOverview.text = seriesCrunchy.description | ||||
|         binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull() | ||||
|  | ||||
|         binding.textYear.text = when(tmdbResult) { | ||||
|             is TMDBTVShow -> (tmdbResult as TMDBTVShow).firstAirDate.substring(0, 4) | ||||
|             is TMDBMovie -> (tmdbResult as TMDBMovie).releaseDate.substring(0, 4) | ||||
|             else -> "" | ||||
|         } | ||||
|  | ||||
|         // TODO set "my list" indicator | ||||
| //        if (StorageController.myList.contains(media.aodId)) { | ||||
| @ -119,8 +124,7 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : | ||||
|         fragments.clear() | ||||
|         pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize) | ||||
|  | ||||
|  | ||||
|         // add the episodes fragment (as tab) | ||||
|         // add the episodes fragment (as tab). Note: Movies are tv shows! | ||||
|         MediaFragmentEpisodes().also { | ||||
|             fragments.add(it) | ||||
|             pagerAdapter.notifyItemInserted(fragments.indexOf(it)) | ||||
| @ -128,6 +132,33 @@ class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : | ||||
|  | ||||
|         // TODO reimplement via tmdb/metaDB | ||||
|         // specific gui | ||||
|         when (tmdbResult) { | ||||
|             is TMDBTVShow -> { | ||||
|                 // episodes count | ||||
|                 binding.textEpisodesOrRuntime.text = resources.getQuantityString( | ||||
|                     R.plurals.text_episodes_count, | ||||
|                     episodesCrunchy.total, | ||||
|                     episodesCrunchy.total | ||||
|                 ) | ||||
|             } | ||||
|             is TMDBMovie -> { | ||||
|                 val tmdbMovie = (tmdbResult as TMDBMovie?) | ||||
|  | ||||
|                 if (tmdbMovie?.runtime != null) { | ||||
|                     binding.textEpisodesOrRuntime.text = resources.getQuantityString( | ||||
|                         R.plurals.text_runtime, | ||||
|                         tmdbMovie.runtime, | ||||
|                         tmdbMovie.runtime | ||||
|                     ) | ||||
|                 } else { | ||||
|                     binding.textEpisodesOrRuntime.visibility = View.GONE | ||||
|                 } | ||||
|             } | ||||
|             else -> { | ||||
|                 println("else") | ||||
|             } | ||||
|         } | ||||
|  | ||||
| //        if (mediaCrunchy.type == MediaType.TVSHOW.str) { | ||||
| //            // TODO get next episode | ||||
| ////            nextEpisodeId = media.playlist.firstOrNull{ !it.watched }?.mediaId | ||||
|  | ||||
| @ -31,7 +31,7 @@ class MediaFragmentEpisodes : Fragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.currentEpisodesCrunchy, model.tmdbTVSeason?.episodes) | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.currentEpisodesCrunchy, model.tmdbTVSeason.episodes) | ||||
|         binding.recyclerEpisodes.adapter = adapterRecEpisodes | ||||
|  | ||||
|         // set onItemClick, adapter is initialized | ||||
| @ -39,10 +39,14 @@ class MediaFragmentEpisodes : Fragment() { | ||||
|             playEpisode(seasonId, episodeId) | ||||
|         } | ||||
|  | ||||
|         // TODO don't show selection if only one season is present | ||||
|         binding.buttonSeasonSelection.text = model.currentSeasonCrunchy.title | ||||
|         binding.buttonSeasonSelection.setOnClickListener { v -> | ||||
|             showSeasonSelection(v) | ||||
|         // don't show season selection if only one season is present | ||||
|         if (model.seasonsCrunchy.total < 2) { | ||||
|             binding.buttonSeasonSelection.visibility = View.GONE | ||||
|         } else { | ||||
|             binding.buttonSeasonSelection.text = model.currentSeasonCrunchy.title | ||||
|             binding.buttonSeasonSelection.setOnClickListener { v -> | ||||
|                 showSeasonSelection(v) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -1,18 +1,15 @@ | ||||
| package org.mosad.teapod.ui.activity.main.viewmodel | ||||
|  | ||||
| import android.app.Application | ||||
| import android.util.Log | ||||
| import androidx.lifecycle.AndroidViewModel | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import kotlinx.coroutines.joinAll | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.serialization.ExperimentalSerializationApi | ||||
| import org.mosad.teapod.parser.crunchyroll.* | ||||
| import org.mosad.teapod.preferences.Preferences | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
| import org.mosad.teapod.util.Meta | ||||
| import org.mosad.teapod.util.tmdb.TMDBApiController | ||||
| import org.mosad.teapod.util.tmdb.TMDBResult | ||||
| import org.mosad.teapod.util.tmdb.TMDBTVSeason | ||||
| import org.mosad.teapod.util.tmdb.* | ||||
|  | ||||
| /** | ||||
|  * handle media, next ep and tmdb | ||||
| @ -22,7 +19,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|  | ||||
| //    var mediaCrunchy = NoneItem | ||||
| //        internal set | ||||
|     var seriesCrunchy = NoneSeries // TODO it seems movies also series? | ||||
|     var seriesCrunchy = NoneSeries // movies are also series | ||||
|         internal set | ||||
|     var seasonsCrunchy = NoneSeasons | ||||
|         internal set | ||||
| @ -32,9 +29,9 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         internal set | ||||
|     val currentEpisodesCrunchy = arrayListOf<Episode>() // used for EpisodeItemAdapter (easier updates) | ||||
|  | ||||
|     var tmdbResult: TMDBResult? = null // TODO rename | ||||
|     var tmdbResult: TMDBResult = NoneTMDB // TODO rename | ||||
|         internal set | ||||
|     var tmdbTVSeason: TMDBTVSeason? =null | ||||
|     var tmdbTVSeason: TMDBTVSeason = NoneTMDBTVSeason | ||||
|         internal set | ||||
|     var mediaMeta: Meta? = null | ||||
|         internal set | ||||
| @ -42,9 +39,8 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|     /** | ||||
|      * @param crunchyId the crunchyroll series id | ||||
|      */ | ||||
|     suspend fun loadCrunchy(crunchyId: String) { | ||||
|         val tmdbApiController = TMDBApiController() | ||||
|  | ||||
|     suspend fun loadCrunchy(crunchyId: String) { | ||||
|         // load series and seasons info in parallel | ||||
|         listOf( | ||||
|             viewModelScope.launch { seriesCrunchy = Crunchyroll.series(crunchyId) }, | ||||
| @ -54,6 +50,8 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         println("series: $seriesCrunchy") | ||||
|         println("seasons: $seasonsCrunchy") | ||||
|  | ||||
|         // TODO load episodes, metaDB and tmdb in parallel | ||||
|  | ||||
|         // load the preferred season (preferred language, language per season, not per stream) | ||||
|         currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocal) | ||||
|         episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) | ||||
| @ -62,18 +60,17 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         println("episodes: $episodesCrunchy") | ||||
|  | ||||
|         // TODO check if metaDB knows the title | ||||
|  | ||||
|         // use tmdb search to get media info TODO media type is hardcoded, use episodeNumber? (if null it should be a movie) | ||||
|         mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media | ||||
|         val tmdbId = tmdbApiController.search(seriesCrunchy.title, MediaType.TVSHOW) | ||||
|  | ||||
|         tmdbResult = when (MediaType.TVSHOW) { | ||||
|             MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) | ||||
|             MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) | ||||
|             else -> null | ||||
|         } | ||||
|         // use tmdb search to get media info | ||||
|         loadTmdbInfo() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set currentSeasonCrunchy based on the season id. Also set the new seasons episodes. | ||||
|      * | ||||
|      * @param seasonId the id of the season to set | ||||
|      */ | ||||
|     suspend fun setCurrentSeason(seasonId: String) { | ||||
|         // return if the id hasn't changed (performance) | ||||
|         if (currentSeasonCrunchy.id == seasonId) return | ||||
| @ -90,49 +87,33 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * set media, tmdb and nextEpisode | ||||
|      * Load the tmdb info for the selected media. | ||||
|      * The TMDB search return a media type, use this to get the details (movie/tv show and season) | ||||
|      */ | ||||
| //    suspend fun loadAoD(aodId: Int) { | ||||
| //        val tmdbApiController = TMDBApiController() | ||||
| //        media = AoDParser.getMediaById(aodId) | ||||
| // | ||||
| //        // check if metaDB knows the title | ||||
| //        val tmdbId: Int = if (MetaDBController.mediaList.media.contains(aodId)) { | ||||
| //            // load media info from metaDB | ||||
| //            val metaDB = MetaDBController() | ||||
| //            mediaMeta = when (media.type) { | ||||
| //                MediaType.MOVIE -> metaDB.getMovieMetadata(media.aodId) | ||||
| //                MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.aodId) | ||||
| //                else -> null | ||||
| //            } | ||||
| // | ||||
| //            mediaMeta?.tmdbId ?: -1 | ||||
| //        } else { | ||||
| //            // use tmdb search to get media info | ||||
| //            mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media | ||||
| //            tmdbApiController.search(stripTitleInfo(media.title), media.type) | ||||
| //        } | ||||
| // | ||||
| //        tmdbResult = when (media.type) { | ||||
| //            MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) | ||||
| //            MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) | ||||
| //            else -> null | ||||
| //        } | ||||
| // | ||||
| //        // get season info, if metaDB knows the tv show | ||||
| //        tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) { | ||||
| //            val tvShowMeta = mediaMeta as TVShowMeta | ||||
| //            tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) | ||||
| //        } else { | ||||
| //            null | ||||
| //        } | ||||
| // | ||||
| //        if (media.type == MediaType.TVSHOW) { | ||||
| //            //nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() | ||||
| //            nextEpisodeId = media.playlist.firstOrNull { !it.watched }?.mediaId | ||||
| //                ?: media.playlist.first().mediaId | ||||
| //        } | ||||
| //    } | ||||
|     @ExperimentalSerializationApi | ||||
|     suspend fun loadTmdbInfo() { | ||||
|         val tmdbApiController = TMDBApiController() | ||||
|  | ||||
|         val tmdbSearchResult = tmdbApiController.searchMulti(seriesCrunchy.title) | ||||
|         println(tmdbSearchResult) | ||||
|  | ||||
|         tmdbResult = if (tmdbSearchResult.results.isNotEmpty()) { | ||||
|             val result = tmdbSearchResult.results.first() | ||||
|  | ||||
|             when (result.mediaType) { | ||||
|                 "movie" -> tmdbApiController.getMovieDetails(result.id) | ||||
|                 "tv" -> tmdbApiController.getTVShowDetails(result.id) | ||||
|                 else -> NoneTMDB | ||||
|             } | ||||
|         } else NoneTMDB | ||||
|  | ||||
|         println(tmdbResult) | ||||
|  | ||||
|         // currently not used | ||||
| //        tmdbTVSeason = if (tmdbResult is TMDBTVShow) { | ||||
| //            tmdbApiController.getTVSeasonDetails(tmdbResult.id, 0) | ||||
| //        } else NoneTMDBTVSeason | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * get the next episode based on episodeId | ||||
| @ -146,4 +127,4 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
| //            ?: media.playlist.first().mediaId | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -5,3 +5,7 @@ import android.widget.TextView | ||||
| fun TextView.setDrawableTop(drawable: Int) { | ||||
|     this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0) | ||||
| } | ||||
|  | ||||
| fun <T> concatenate(vararg lists: List<T>): List<T> { | ||||
|     return listOf(*lists).flatten() | ||||
| } | ||||
|  | ||||
| @ -22,116 +22,113 @@ | ||||
|  | ||||
| package org.mosad.teapod.util.tmdb | ||||
|  | ||||
| import android.util.Log | ||||
| import com.google.gson.Gson | ||||
| import com.google.gson.JsonParser | ||||
| import com.github.kittinunf.fuel.Fuel | ||||
| import com.github.kittinunf.fuel.core.FuelError | ||||
| import com.github.kittinunf.fuel.core.Parameters | ||||
| import com.github.kittinunf.fuel.json.FuelJson | ||||
| import com.github.kittinunf.fuel.json.responseJson | ||||
| import com.github.kittinunf.result.Result | ||||
| import kotlinx.coroutines.* | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
| import java.io.FileNotFoundException | ||||
| import java.net.URL | ||||
| import java.net.URLEncoder | ||||
| import kotlinx.serialization.ExperimentalSerializationApi | ||||
| import kotlinx.serialization.decodeFromString | ||||
| import kotlinx.serialization.json.Json | ||||
| import org.mosad.teapod.util.concatenate | ||||
|  | ||||
| /** | ||||
|  * Controller for tmdb api integration. | ||||
|  * Data types are in TMDBDataTypes. For the type definitions see: | ||||
|  * https://developers.themoviedb.org/3/getting-started/introduction | ||||
|  * | ||||
|  * TODO evaluate Klaxon | ||||
|  */ | ||||
| class TMDBApiController { | ||||
|  | ||||
|     private val json = Json { ignoreUnknownKeys = true } | ||||
|  | ||||
|     private val apiUrl = "https://api.themoviedb.org/3" | ||||
|     private val searchMovieUrl = "$apiUrl/search/movie" | ||||
|     private val searchTVUrl = "$apiUrl/search/tv" | ||||
|     private val detailsMovieUrl = "$apiUrl/movie" | ||||
|     private val detailsTVUrl = "$apiUrl/tv" | ||||
|     private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2" | ||||
|     private val language = "de" | ||||
|     private val preparedParameters = "?api_key=$apiKey&language=$language" | ||||
|  | ||||
|     companion object{ | ||||
|         const val imageUrl = "https://image.tmdb.org/t/p/w500" | ||||
|     } | ||||
|  | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     private suspend fun request( | ||||
|         endpoint: String, | ||||
|         parameters: Parameters = emptyList() | ||||
|     ): Result<FuelJson, FuelError> = coroutineScope { | ||||
|         val path = "$apiUrl$endpoint" | ||||
|         val params = concatenate(listOf("api_key" to apiKey, "language" to language), parameters) | ||||
|  | ||||
|         // TODO handle FileNotFoundException | ||||
|         return@coroutineScope (Dispatchers.IO) { | ||||
|             val (_, _, result) = Fuel.get(path, params) | ||||
|                 .responseJson() | ||||
|  | ||||
|             result | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Search for a media(movie or tv show) in tmdb | ||||
|      * @param query The query text | ||||
|      * @param type The media type (movie or tv show) | ||||
|      * @return The media tmdb id, or -1 if not found | ||||
|      * @return A TMDBSearch object, or NoneTMDBSearch if nothing was found | ||||
|      */ | ||||
|     suspend fun search(query: String, type: MediaType): Int = withContext(Dispatchers.IO) { | ||||
|         val searchUrl = when (type) { | ||||
|             MediaType.MOVIE -> searchMovieUrl | ||||
|             MediaType.TVSHOW -> searchTVUrl | ||||
|             else -> { | ||||
|                 Log.e(javaClass.name, "Wrong Type: $type") | ||||
|                 return@withContext -1 | ||||
|             } | ||||
|         } | ||||
|     @ExperimentalSerializationApi | ||||
|     suspend fun searchMulti(query: String): TMDBSearch { | ||||
|         val searchEndpoint = "/search/multi" | ||||
|         val parameters = listOf("query" to query, "include_adult" to false) | ||||
|  | ||||
|         val url = URL("$searchUrl$preparedParameters&query=${URLEncoder.encode(query, "UTF-8")}") | ||||
|         val response = JsonParser.parseString(url.readText()).asJsonObject | ||||
|         val sortedResults = response.get("results").asJsonArray.toList().sortedBy { | ||||
|             it.asJsonObject.get("title")?.asString | ||||
|         } | ||||
|  | ||||
|         return@withContext sortedResults.firstOrNull()?.asJsonObject?.get("id")?.asInt ?: -1 | ||||
|         val result = request(searchEndpoint, parameters) | ||||
|         return result.component1()?.obj()?.let { | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneTMDBSearch | ||||
|     } | ||||
|  | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     /** | ||||
|      * Get details for a movie from tmdb | ||||
|      * @param movieId The tmdb ID of the movie | ||||
|      * @return A tmdb movie object, or null if not found | ||||
|      * @return A TMDBMovie object, or NoneTMDBMovie if not found | ||||
|      */ | ||||
|     suspend fun getMovieDetails(movieId: Int): TMDBMovie? = withContext(Dispatchers.IO) { | ||||
|         val url = URL("$detailsMovieUrl/$movieId?api_key=$apiKey&language=$language") | ||||
|     suspend fun getMovieDetails(movieId: Int): TMDBMovie { | ||||
|         val movieEndpoint = "/movie/$movieId" | ||||
|  | ||||
|         return@withContext try { | ||||
|             val json = url.readText() | ||||
|             Gson().fromJson(json, TMDBMovie::class.java) | ||||
|         } catch (ex: FileNotFoundException) { | ||||
|             Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $movieId", ex) | ||||
|             null | ||||
|         } | ||||
|         // TODO is FileNotFoundException handling needed? | ||||
|         val result = request(movieEndpoint) | ||||
|         return result.component1()?.obj()?.let { | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneTMDBMovie | ||||
|     } | ||||
|  | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     /** | ||||
|      * Get details for a tv show from tmdb | ||||
|      * @param tvId The tmdb ID of the tv show | ||||
|      * @return A tmdb tv show object, or null if not found | ||||
|      * @return A TMDBTVShow object, or NoneTMDBTVShow if not found | ||||
|      */ | ||||
|     suspend fun getTVShowDetails(tvId: Int): TMDBTVShow? = withContext(Dispatchers.IO) { | ||||
|         val url = URL("$detailsTVUrl/$tvId?api_key=$apiKey&language=$language") | ||||
|     suspend fun getTVShowDetails(tvId: Int): TMDBTVShow { | ||||
|         val tvShowEndpoint = "/tv/$tvId" | ||||
|  | ||||
|         return@withContext try { | ||||
|             val json = url.readText() | ||||
|             Gson().fromJson(json, TMDBTVShow::class.java) | ||||
|         } catch (ex: FileNotFoundException) { | ||||
|             Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $tvId", ex) | ||||
|             null | ||||
|         } | ||||
|         // TODO is FileNotFoundException handling needed? | ||||
|         val result = request(tvShowEndpoint) | ||||
|         return result.component1()?.obj()?.let { | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneTMDBTVShow | ||||
|     } | ||||
|  | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     @Suppress("unused") | ||||
|     /** | ||||
|      * Get details for a tv show season from tmdb | ||||
|      * @param tvId The tmdb ID of the tv show | ||||
|      * @param seasonNumber The tmdb season number | ||||
|      * @return A tmdb tv season object, or null if not found | ||||
|      * @return A TMDBTVSeason object, or NoneTMDBTVSeason if not found | ||||
|      */ | ||||
|     suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TMDBTVSeason? = withContext(Dispatchers.IO) { | ||||
|         val url = URL("$detailsTVUrl/$tvId/season/$seasonNumber?api_key=$apiKey&language=$language") | ||||
|     suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TMDBTVSeason { | ||||
|         val tvShowSeasonEndpoint = "/tv/$tvId/season/$seasonNumber" | ||||
|  | ||||
|         return@withContext try { | ||||
|             val json = url.readText() | ||||
|             Gson().fromJson(json, TMDBTVSeason::class.java) | ||||
|         } catch (ex: FileNotFoundException) { | ||||
|             Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $tvId, Season: $seasonNumber", ex) | ||||
|             null | ||||
|         } | ||||
|         // TODO is FileNotFoundException handling needed? | ||||
|         val result = request(tvShowSeasonEndpoint) | ||||
|         return result.component1()?.obj()?.let { | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneTMDBTVSeason | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -22,71 +22,110 @@ | ||||
|  | ||||
| package org.mosad.teapod.util.tmdb | ||||
|  | ||||
| import com.google.gson.annotations.SerializedName | ||||
| import android.os.Build | ||||
| import androidx.annotation.RequiresApi | ||||
| import kotlinx.serialization.* | ||||
| import kotlinx.serialization.json.JsonNames | ||||
| import java.text.DateFormat | ||||
| import java.time.LocalDate | ||||
| import java.util.* | ||||
|  | ||||
| /** | ||||
|  * New TMDB API data classes | ||||
|  */ | ||||
|  | ||||
| @ExperimentalSerializationApi | ||||
| @Serializable | ||||
| data class TMDBSearch( | ||||
|     val page: Int, | ||||
|     val results: List<TMDBSearchResult> | ||||
| ) | ||||
|  | ||||
| @ExperimentalSerializationApi | ||||
| @Serializable | ||||
| data class TMDBSearchResult( | ||||
|     @SerialName("id") val id: Int, | ||||
|     @SerialName("media_type") val mediaType: String, | ||||
|     @JsonNames("name", "title") val name: String, // tv show = name, movie = title | ||||
|     @SerialName("overview") val overview: String?, | ||||
|     @SerialName("poster_path") val posterPath: String?, | ||||
|     @SerialName("backdrop_path") val backdropPath: String?, | ||||
| ) | ||||
|  | ||||
| @ExperimentalSerializationApi | ||||
| val NoneTMDBSearch = TMDBSearch(0, emptyList()) | ||||
|  | ||||
| /** | ||||
|  * These data classes represent the tmdb api json objects. | ||||
|  * Fields which are nullable in the tmdb api are also nullable here. | ||||
|  */ | ||||
|  | ||||
| abstract class TMDBResult{ | ||||
|     abstract val id: Int | ||||
|     abstract val name: String | ||||
|     abstract val overview: String? // for movies tmdb return string or null | ||||
|     abstract val posterPath: String? | ||||
|     abstract val backdropPath: String? | ||||
| interface TMDBResult { | ||||
|     val id: Int | ||||
|     val name: String | ||||
|     val overview: String? // for movies tmdb return string or null | ||||
|     val posterPath: String? | ||||
|     val backdropPath: String? | ||||
| } | ||||
|  | ||||
| data class TMDBMovie( | ||||
| data class TMDBBase( | ||||
|     override val id: Int, | ||||
|     override val name: String, | ||||
|     override val overview: String?, | ||||
|     @SerializedName("poster_path") | ||||
|     override val posterPath: String?, | ||||
|     @SerializedName("backdrop_path") | ||||
|     override val backdropPath: String?, | ||||
|     @SerializedName("release_date") | ||||
|     val releaseDate: String, | ||||
|     @SerializedName("runtime") | ||||
|     val runtime: Int?, | ||||
|     // TODO generes | ||||
| ): TMDBResult() | ||||
|     override val backdropPath: String? | ||||
| ) : TMDBResult | ||||
|  | ||||
| @Serializable | ||||
| data class TMDBMovie( | ||||
|     @SerialName("id") override val id: Int, | ||||
|     @SerialName("title") override val name: String, // for movies the name is in the field title | ||||
|     @SerialName("overview") override val overview: String?, | ||||
|     @SerialName("poster_path") override val posterPath: String?, | ||||
|     @SerialName("backdrop_path") override val backdropPath: String?, | ||||
|     @SerialName("release_date") val releaseDate: String, | ||||
|     @SerialName("runtime") val runtime: Int?, | ||||
|     @SerialName("status") val status: String, | ||||
|     // TODO generes | ||||
| ) : TMDBResult | ||||
|  | ||||
| @Serializable | ||||
| data class TMDBTVShow( | ||||
|     override val id: Int, | ||||
|     override val name: String, | ||||
|     override val overview: String, | ||||
|     @SerializedName("poster_path") | ||||
|     override val posterPath: String?, | ||||
|     @SerializedName("backdrop_path") | ||||
|     override val backdropPath: String?, | ||||
|     @SerializedName("first_air_date") | ||||
|     val firstAirDate: String, | ||||
|     @SerializedName("status") | ||||
|     val status: String, | ||||
|     @SerialName("id")override val id: Int, | ||||
|     @SerialName("name")override val name: String, | ||||
|     @SerialName("overview")override val overview: String, | ||||
|     @SerialName("poster_path") override val posterPath: String?, | ||||
|     @SerialName("backdrop_path") override val backdropPath: String?, | ||||
|     @SerialName("first_air_date") val firstAirDate: String, | ||||
|     @SerialName("last_air_date") val lastAirDate: String, | ||||
|     @SerialName("status") val status: String, | ||||
|     // TODO generes | ||||
| ): TMDBResult() | ||||
| ) : TMDBResult | ||||
|  | ||||
| // use null for nullable types, the gui needs to handle/implement a fallback for null values | ||||
| val NoneTMDB = TMDBBase(0, "", "", null, null) | ||||
| val NoneTMDBMovie = TMDBMovie(0, "", "", null, null, "", null, "") | ||||
| val NoneTMDBTVShow = TMDBTVShow(0, "", "", null, null, "", "", "") | ||||
|  | ||||
| @Serializable | ||||
| data class TMDBTVSeason( | ||||
|     val id: Int, | ||||
|     val name: String, | ||||
|     val overview: String, | ||||
|     @SerializedName("poster_path") | ||||
|     val posterPath: String?, | ||||
|     @SerializedName("air_date") | ||||
|     val airDate: String, | ||||
|     @SerializedName("episodes") | ||||
|     val episodes: List<TMDBTVEpisode>, | ||||
|     @SerializedName("season_number") | ||||
|     val seasonNumber: Int | ||||
|     @SerialName("id") val id: Int, | ||||
|     @SerialName("name") val name: String, | ||||
|     @SerialName("overview") val overview: String, | ||||
|     @SerialName("poster_path") val posterPath: String?, | ||||
|     @SerialName("air_date") val airDate: String, | ||||
|     @SerialName("episodes") val episodes: List<TMDBTVEpisode>, | ||||
|     @SerialName("season_number") val seasonNumber: Int | ||||
| ) | ||||
|  | ||||
| @Serializable | ||||
| data class TMDBTVEpisode( | ||||
|     val id: Int, | ||||
|     val name: String, | ||||
|     val overview: String, | ||||
|     @SerializedName("air_date") | ||||
|     val airDate: String, | ||||
|     @SerializedName("episode_number") | ||||
|     val episodeNumber: Int | ||||
| ) | ||||
|     @SerialName("id") val id: Int, | ||||
|     @SerialName("name") val name: String, | ||||
|     @SerialName("overview") val overview: String, | ||||
|     @SerialName("air_date") val airDate: String, | ||||
|     @SerialName("episode_number") val episodeNumber: Int | ||||
| ) | ||||
|  | ||||
| // use null for nullable types, the gui needs to handle/implement a fallback for null values | ||||
| val NoneTMDBTVSeason = TMDBTVSeason(0, "", "", null, "", emptyList(), 0) | ||||
|  | ||||
		Reference in New Issue
	
	Block a user