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