use tmdb data if missing on aod

*  episode description
This commit is contained in:
Jannik 2021-07-11 12:56:21 +02:00
parent 44f99295e9
commit c66c725ee3
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
9 changed files with 215 additions and 41 deletions

View File

@ -46,6 +46,7 @@ import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
import org.mosad.teapod.ui.activity.player.PlayerActivity import org.mosad.teapod.ui.activity.player.PlayerActivity
import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.MetaDBController
import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.exitAndRemoveTask import org.mosad.teapod.util.exitAndRemoveTask
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@ -137,8 +138,12 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
*/ */
private fun load() { private fun load() {
val time = measureTimeMillis { val time = measureTimeMillis {
// start the initial loading
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope")) val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
.async { AoDParser.initialLoading() } // start the initial loading .async {
launch { AoDParser.initialLoading() }
launch { MetaDBController.list() }
}
// load all saved stuff here // load all saved stuff here
Preferences.load(this) Preferences.load(this)

View File

@ -64,7 +64,6 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
} }
}.attach() }.attach()
lifecycleScope.launch { lifecycleScope.launch {
model.load(mediaId) // load the streams and tmdb for the selected media model.load(mediaId) // load the streams and tmdb for the selected media

View File

@ -28,7 +28,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.media.episodes) adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes, model.tmdbTVSeason?.episodes)
binding.recyclerEpisodes.adapter = adapterRecEpisodes binding.recyclerEpisodes.adapter = adapterRecEpisodes
// set onItemClick only in adapter is initialized // set onItemClick only in adapter is initialized

View File

@ -9,9 +9,11 @@ import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.tmdb.Movie import org.mosad.teapod.util.tmdb.Movie
import org.mosad.teapod.util.tmdb.TMDBApiController import org.mosad.teapod.util.tmdb.TMDBApiController
import org.mosad.teapod.util.tmdb.TMDBResult import org.mosad.teapod.util.tmdb.TMDBResult
import org.mosad.teapod.util.tmdb.TVSeason
/** /**
* handle media, next ep and tmdb * handle media, next ep and tmdb
* TODO this lives in activity, is this correct?
*/ */
class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) {
@ -21,16 +23,35 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
internal set internal set
lateinit var tmdbResult: TMDBResult // TODO rename lateinit var tmdbResult: TMDBResult // TODO rename
internal set internal set
var tmdbTVSeason: TVSeason? =null
internal set
var mediaMeta: Meta? = null
internal set
/** /**
* set media, tmdb and nextEpisode * set media, tmdb and nextEpisode
* TODO run aod and tmdb load parallel
*/ */
suspend fun load(mediaId: Int) { suspend fun load(mediaId: Int) {
val tmdbApiController = TMDBApiController()
media = AoDParser.getMediaById(mediaId) media = AoDParser.getMediaById(mediaId)
val tmdbApiController = TMDBApiController() // check if metaDB knows the title
val searchTitle = stripTitleInfo(media.info.title) val tmdbId: Int = if (MetaDBController.mediaList.media.contains(media.id)) {
val tmdbId = tmdbApiController.search(searchTitle, media.type) // load media info from metaDB
val metaDB = MetaDBController()
mediaMeta = when (media.type) {
MediaType.MOVIE -> metaDB.getMovieMetadata(media.id)
MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.id)
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.info.title), media.type)
}
tmdbResult = when (media.type) { tmdbResult = when (media.type) {
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)
@ -39,16 +60,30 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
} }
println(tmdbResult) // TODO println(tmdbResult) // TODO
// TESTING // get season info, if metaDB knows the tv show
if (media.type == MediaType.TVSHOW) { tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) {
val seasonNumber = guessSeasonFromTitle(media.info.title) val tvShowMeta = mediaMeta as TVShowMeta
Log.d("test", "season number: $seasonNumber") tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
} else {
// TODO Important: only use tmdb info if media title and episode number match exactly null
val tmdbTVSeason = tmdbApiController.getTVSeasonDetails(tmdbId, seasonNumber)
Log.d("test", "Season Info: $tmdbTVSeason.")
} }
// TESTING
// if (media.type == MediaType.TVSHOW) {
// if (mediaMeta != null) {
// val tvShowMeta = mediaMeta as TVShowMeta
// val tmdbTVSeason = tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
// } else {
// // for tv shows not in metaDB, try to guess/search
//
// val seasonNumber = guessSeasonFromTitle(media.info.title)
// Log.d("test", "season number: $seasonNumber")
//
// val tmdbTVSeason = tmdbApiController.getTVSeasonDetails(tmdbId, seasonNumber)
// Log.d("test", "Season Info: $tmdbTVSeason.")
// }
// }
// TESTING END // TESTING END
if (media.type == MediaType.TVSHOW) { if (media.type == MediaType.TVSHOW) {

View File

@ -19,9 +19,7 @@ import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.*
import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.Media
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -45,6 +43,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
internal set internal set
var nextEpisode: Episode? = null var nextEpisode: Episode? = null
internal set internal set
var mediaMeta: Meta? = null
internal set
var currentLanguage: Locale = Locale.ROOT var currentLanguage: Locale = Locale.ROOT
internal set internal set
@ -75,6 +75,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
fun loadMedia(mediaId: Int, episodeId: Int) { fun loadMedia(mediaId: Int, episodeId: Int) {
runBlocking { runBlocking {
media = AoDParser.getMediaById(mediaId) media = AoDParser.getMediaById(mediaId)
mediaMeta = loadMediaMeta(media.id)
} }
currentEpisode = media.getEpisodeById(episodeId) currentEpisode = media.getEpisodeById(episodeId)
@ -159,6 +160,14 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
} }
} }
private suspend fun loadMediaMeta(aodId: Int): Meta? {
return if (media.type == DataTypes.MediaType.TVSHOW) {
MetaDBController().getTVShowMetadata(aodId)
} else {
null
}
}
/** /**
* Based on the current episodeId, get the next episode. If there is no next * Based on the current episodeId, get the next episode. If there is no next
* episode, return null * episode, return null

View File

@ -0,0 +1,119 @@
package org.mosad.teapod.util
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.*
import java.io.FileNotFoundException
import java.net.URL
class MetaDBController {
companion object {
private const val repoUrl = "https://gitlab.com/Seil0/teapodmetadb/-/raw/main/aod/"
var mediaList = MediaList(listOf())
private var metaCacheList = arrayListOf<Meta>()
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun list() = withContext(Dispatchers.IO) {
val url = URL("$repoUrl/list.json")
val json = url.readText()
Thread.sleep(5000)
mediaList = Gson().fromJson(json, MediaList::class.java)
}
}
suspend fun getMovieMetadata(aodId: Int): MovieMeta? {
return metaCacheList.firstOrNull {
it.aodId == aodId
} as MovieMeta? ?: getMovieMetadata2(aodId)
}
suspend fun getTVShowMetadata(aodId: Int): TVShowMeta? {
return metaCacheList.firstOrNull {
it.aodId == aodId
} as TVShowMeta? ?: getTVShowMetadata2(aodId)
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun getMovieMetadata2(aodId: Int): MovieMeta? = withContext(Dispatchers.IO) {
val url = URL("$repoUrl/movie/$aodId/media.json")
return@withContext try {
val json = url.readText()
val meta = Gson().fromJson(json, MovieMeta::class.java)
metaCacheList.add(meta)
meta
} catch (ex: FileNotFoundException) {
null
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun getTVShowMetadata2(aodId: Int): TVShowMeta? = withContext(Dispatchers.IO) {
val url = URL("$repoUrl/tv/$aodId/media.json")
return@withContext try {
val json = url.readText()
val meta = Gson().fromJson(json, TVShowMeta::class.java)
metaCacheList.add(meta)
meta
} catch (ex: FileNotFoundException) {
null
}
}
}
// TODO move data classes
data class MediaList(
val media: List<Int>
)
abstract class Meta {
abstract val id: Int
abstract val aodId: Int
abstract val tmdbId: Int
}
data class MovieMeta(
override val id: Int,
@SerializedName("aod_id")
override val aodId: Int,
@SerializedName("tmdb_id")
override val tmdbId: Int
): Meta()
data class TVShowMeta(
override val id: Int,
@SerializedName("aod_id")
override val aodId: Int,
@SerializedName("tmdb_id")
override val tmdbId: Int,
@SerializedName("tmdb_season_id")
val tmdbSeasonId: Int,
@SerializedName("tmdb_season_number")
val tmdbSeasonNumber: Int,
@SerializedName("episodes")
val episodes: List<EpisodeMeta>
): Meta()
data class EpisodeMeta(
val id: Int,
@SerializedName("aod_media_id")
val aodMediaId: Int,
@SerializedName("tmdb_id")
val tmdbId: Int,
@SerializedName("tmdb_number")
val tmdbNumber: Int,
@SerializedName("opening_start")
val openingStart: Int,
@SerializedName("opening_duration")
val openingDuration: Int,
@SerializedName("ending_start")
val endingStart: Int,
@SerializedName("ending_duration")
val endingDuration: Int
)

View File

@ -12,8 +12,9 @@ import jp.wasabeef.glide.transformations.RoundedCornersTransformation
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ItemEpisodeBinding import org.mosad.teapod.databinding.ItemEpisodeBinding
import org.mosad.teapod.util.Episode import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.tmdb.TVEpisode
class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() { class EpisodeItemAdapter(private val episodes: List<Episode>, private val tmdbEpisodes: List<TVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() {
var onImageClick: ((String, Int) -> Unit)? = null var onImageClick: ((String, Int) -> Unit)? = null
@ -32,7 +33,13 @@ class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Ada
} }
holder.binding.textEpisodeTitle.text = titleText holder.binding.textEpisodeTitle.text = titleText
holder.binding.textEpisodeDesc.text = ep.shortDesc holder.binding.textEpisodeDesc.text = if (ep.shortDesc.isNotEmpty()) {
ep.shortDesc
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
tmdbEpisodes[position].overview
} else {
""
}
if (episodes[position].posterUrl.isNotEmpty()) { if (episodes[position].posterUrl.isNotEmpty()) {
Glide.with(context).load(ep.posterUrl) Glide.with(context).load(ep.posterUrl)

View File

@ -99,14 +99,14 @@ class TMDBApiController {
} }
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TVSeason = withContext(Dispatchers.IO) { suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TVSeason? = withContext(Dispatchers.IO) {
val url = URL("$detailsTVUrl/$tvId/season/$seasonNumber?api_key=$apiKey&language=$language") val url = URL("$detailsTVUrl/$tvId/season/$seasonNumber?api_key=$apiKey&language=$language")
val response = try { val response = try {
JsonParser.parseString(url.readText()).asJsonObject JsonParser.parseString(url.readText()).asJsonObject
} catch (ex: FileNotFoundException) { } catch (ex: FileNotFoundException) {
Log.w(javaClass.name, "The resource you requested could not be found") Log.w(javaClass.name, "The resource you requested could not be found")
return@withContext TVSeason(-1) return@withContext null
} }
// println(response) // println(response)
@ -114,25 +114,25 @@ class TMDBApiController {
val episodes = response.get("episodes").asJsonArray.map { val episodes = response.get("episodes").asJsonArray.map {
TVEpisode( TVEpisode(
id = it.asJsonObject.get("id").asInt, id = it.asJsonObject.get("id").asInt,
name = it.asJsonObject.get("name")?.asString, name = it.asJsonObject.get("name")?.asString ?: "",
overview = it.asJsonObject.get("overview")?.asString, overview = it.asJsonObject.get("overview")?.asString ?: "",
airDate = it.asJsonObject.get("air_date")?.asString, airDate = it.asJsonObject.get("air_date")?.asString ?: "",
episodeNumber = it.asJsonObject.get("episode_number")?.asInt episodeNumber = it.asJsonObject.get("episode_number")?.asInt ?: -1
) )
} }
TVSeason( TVSeason(
id = response.get("id").asInt, id = response.get("id").asInt,
name = response.asJsonObject.get("name")?.asString, name = response.asJsonObject.get("name")?.asString ?: "",
overview = response.asJsonObject.get("overview")?.asString, overview = response.asJsonObject.get("overview")?.asString ?: "",
posterPath = response.asJsonObject.get("poster_path")?.asString, posterPath = response.asJsonObject.get("poster_path")?.asString ?: "",
airDate = response.asJsonObject.get("air_date")?.asString, airDate = response.asJsonObject.get("air_date")?.asString ?: "",
episodes = episodes, episodes = episodes,
seasonNumber = response.get("season_number")?.asInt seasonNumber = response.get("season_number")?.asInt ?: -1
) )
} catch (ex: Exception) { } catch (ex: Exception) {
Log.w(javaClass.name, "Error", ex) Log.w(javaClass.name, "Error", ex)
TVSeason(-1) null
} }
} }

View File

@ -32,19 +32,19 @@ data class TVShow(
data class TVSeason( data class TVSeason(
val id: Int, val id: Int,
val name: String? = null, val name: String,
val overview: String? = null, val overview: String,
val posterPath: String? = null, val posterPath: String,
val airDate: String? = null, val airDate: String,
val episodes: List<TVEpisode>? = null, val episodes: List<TVEpisode>,
val seasonNumber: Int? = null val seasonNumber: Int
) )
// TODO decide whether to use nullable or not // TODO decide whether to use nullable or not
data class TVEpisode( data class TVEpisode(
val id: Int, val id: Int,
val name: String? = null, val name: String,
val overview: String? = null, val overview: String,
val airDate: String? = null, val airDate: String,
val episodeNumber: Int? = null val episodeNumber: Int
) )