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