diff --git a/app/build.gradle b/app/build.gradle index 0233747..9d9704f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1") + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1' implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.1' diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt index 1fc542f..b316816 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt @@ -76,6 +76,8 @@ object Crunchyroll { ): Result = 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 } diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt index 4d8720a..744ba52 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt @@ -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 ) -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 + @SerialName("total") val total: Int, + @SerialName("items") val items: List ) { 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) +data class Episodes( + @SerialName("total") val total: Int, + @SerialName("items") val items: List +) @Serializable data class Episode( diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt index f9caed4..d524067 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt @@ -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 diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt index 8682060..f680356 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt @@ -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) + } } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt index adc6b25..d8eb5c1 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt @@ -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() // 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 } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/mosad/teapod/util/Utils.kt b/app/src/main/java/org/mosad/teapod/util/Utils.kt index 1b9e7f8..bd284e6 100644 --- a/app/src/main/java/org/mosad/teapod/util/Utils.kt +++ b/app/src/main/java/org/mosad/teapod/util/Utils.kt @@ -5,3 +5,7 @@ import android.widget.TextView fun TextView.setDrawableTop(drawable: Int) { this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0) } + +fun concatenate(vararg lists: List): List { + return listOf(*lists).flatten() +} diff --git a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt index 93003c4..5d6f76c 100644 --- a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt @@ -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 = 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 } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt index a3f5106..9e9c073 100644 --- a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt @@ -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 +) + +@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, - @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, + @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 -) \ No newline at end of file + @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)