move TMDBApiCOntroller to Fuel and kotlinx.serialization
* add year and maturityRatings to MediaFragment * don't show season selection if only one season is presentpull/49/head
parent
27e7f2a249
commit
554af530e3
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue