use tmdb data if missing on aod
* episode description
This commit is contained in:
		| @ -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.components.LoginDialog | ||||
| import org.mosad.teapod.util.DataTypes | ||||
| import org.mosad.teapod.util.MetaDBController | ||||
| import org.mosad.teapod.util.StorageController | ||||
| import org.mosad.teapod.util.exitAndRemoveTask | ||||
| import java.net.SocketTimeoutException | ||||
| @ -137,8 +138,12 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen | ||||
|      */ | ||||
|     private fun load() { | ||||
|         val time = measureTimeMillis { | ||||
|             // start the initial loading | ||||
|             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 | ||||
|             Preferences.load(this) | ||||
|  | ||||
| @ -64,7 +64,6 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|             } | ||||
|         }.attach() | ||||
|  | ||||
|  | ||||
|         lifecycleScope.launch { | ||||
|             model.load(mediaId) // load the streams and tmdb for the selected media | ||||
|  | ||||
|  | ||||
| @ -28,7 +28,7 @@ class MediaFragmentEpisodes : Fragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes) | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes, model.tmdbTVSeason?.episodes) | ||||
|         binding.recyclerEpisodes.adapter = adapterRecEpisodes | ||||
|  | ||||
|         // set onItemClick only in adapter is initialized | ||||
|  | ||||
| @ -9,9 +9,11 @@ import org.mosad.teapod.util.DataTypes.MediaType | ||||
| import org.mosad.teapod.util.tmdb.Movie | ||||
| import org.mosad.teapod.util.tmdb.TMDBApiController | ||||
| import org.mosad.teapod.util.tmdb.TMDBResult | ||||
| import org.mosad.teapod.util.tmdb.TVSeason | ||||
|  | ||||
| /** | ||||
|  * handle media, next ep and tmdb | ||||
|  * TODO this lives in activity, is this correct? | ||||
|  */ | ||||
| class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { | ||||
|  | ||||
| @ -21,16 +23,35 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         internal set | ||||
|     lateinit var tmdbResult: TMDBResult // TODO rename | ||||
|         internal set | ||||
|     var tmdbTVSeason: TVSeason? =null | ||||
|         internal set | ||||
|     var mediaMeta: Meta? = null | ||||
|         internal set | ||||
|  | ||||
|     /** | ||||
|      * set media, tmdb and nextEpisode | ||||
|      * TODO run aod and tmdb load parallel | ||||
|      */ | ||||
|     suspend fun load(mediaId: Int) { | ||||
|         val tmdbApiController = TMDBApiController() | ||||
|         media = AoDParser.getMediaById(mediaId) | ||||
|  | ||||
|         val tmdbApiController = TMDBApiController() | ||||
|         val searchTitle = stripTitleInfo(media.info.title) | ||||
|         val tmdbId = tmdbApiController.search(searchTitle, media.type) | ||||
|         // check if metaDB knows the title | ||||
|         val tmdbId: Int = if (MetaDBController.mediaList.media.contains(media.id)) { | ||||
|             // 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) { | ||||
|             MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) | ||||
| @ -39,16 +60,30 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         } | ||||
|         println(tmdbResult) // TODO | ||||
|  | ||||
|         // TESTING | ||||
|         if (media.type == MediaType.TVSHOW) { | ||||
|             val seasonNumber = guessSeasonFromTitle(media.info.title) | ||||
|             Log.d("test", "season number: $seasonNumber") | ||||
|  | ||||
|             // TODO Important: only use tmdb info if media title and episode number match exactly | ||||
|             val tmdbTVSeason = tmdbApiController.getTVSeasonDetails(tmdbId, seasonNumber) | ||||
|             Log.d("test", "Season Info: $tmdbTVSeason.") | ||||
|         // 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 | ||||
|         } | ||||
|  | ||||
|         // 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 | ||||
|  | ||||
|         if (media.type == MediaType.TVSHOW) { | ||||
|  | ||||
| @ -19,9 +19,7 @@ import kotlinx.coroutines.runBlocking | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.preferences.Preferences | ||||
| import org.mosad.teapod.util.DataTypes | ||||
| import org.mosad.teapod.util.Episode | ||||
| import org.mosad.teapod.util.Media | ||||
| import org.mosad.teapod.util.* | ||||
| import java.util.* | ||||
| import kotlin.collections.ArrayList | ||||
|  | ||||
| @ -45,6 +43,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) | ||||
|         internal set | ||||
|     var nextEpisode: Episode? = null | ||||
|         internal set | ||||
|     var mediaMeta: Meta? = null | ||||
|         internal set | ||||
|     var currentLanguage: Locale = Locale.ROOT | ||||
|         internal set | ||||
|  | ||||
| @ -75,6 +75,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) | ||||
|     fun loadMedia(mediaId: Int, episodeId: Int) { | ||||
|         runBlocking { | ||||
|             media = AoDParser.getMediaById(mediaId) | ||||
|             mediaMeta = loadMediaMeta(media.id) | ||||
|         } | ||||
|  | ||||
|         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 | ||||
|      * episode, return null | ||||
|  | ||||
							
								
								
									
										119
									
								
								app/src/main/java/org/mosad/teapod/util/MetaDBController.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								app/src/main/java/org/mosad/teapod/util/MetaDBController.kt
									
									
									
									
									
										Normal 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 | ||||
| ) | ||||
| @ -12,8 +12,9 @@ import jp.wasabeef.glide.transformations.RoundedCornersTransformation | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.ItemEpisodeBinding | ||||
| 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 | ||||
|  | ||||
| @ -32,7 +33,13 @@ class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Ada | ||||
|         } | ||||
|  | ||||
|         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()) { | ||||
|             Glide.with(context).load(ep.posterUrl) | ||||
|  | ||||
| @ -99,14 +99,14 @@ class TMDBApiController { | ||||
|     } | ||||
|  | ||||
|     @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 response = try { | ||||
|             JsonParser.parseString(url.readText()).asJsonObject | ||||
|         } catch (ex: FileNotFoundException) { | ||||
|             Log.w(javaClass.name, "The resource you requested could not be found") | ||||
|             return@withContext TVSeason(-1) | ||||
|             return@withContext null | ||||
|         } | ||||
|         // println(response) | ||||
|  | ||||
| @ -114,25 +114,25 @@ class TMDBApiController { | ||||
|             val episodes = response.get("episodes").asJsonArray.map { | ||||
|                 TVEpisode( | ||||
|                     id = it.asJsonObject.get("id").asInt, | ||||
|                     name = it.asJsonObject.get("name")?.asString, | ||||
|                     overview = it.asJsonObject.get("overview")?.asString, | ||||
|                     airDate = it.asJsonObject.get("air_date")?.asString, | ||||
|                     episodeNumber = it.asJsonObject.get("episode_number")?.asInt | ||||
|                     name = it.asJsonObject.get("name")?.asString ?: "", | ||||
|                     overview = it.asJsonObject.get("overview")?.asString ?: "", | ||||
|                     airDate = it.asJsonObject.get("air_date")?.asString ?: "", | ||||
|                     episodeNumber = it.asJsonObject.get("episode_number")?.asInt ?: -1 | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             TVSeason( | ||||
|                 id = response.get("id").asInt, | ||||
|                 name = response.asJsonObject.get("name")?.asString, | ||||
|                 overview = response.asJsonObject.get("overview")?.asString, | ||||
|                 posterPath = response.asJsonObject.get("poster_path")?.asString, | ||||
|                 airDate = response.asJsonObject.get("air_date")?.asString, | ||||
|                 name = response.asJsonObject.get("name")?.asString ?: "", | ||||
|                 overview = response.asJsonObject.get("overview")?.asString ?: "", | ||||
|                 posterPath = response.asJsonObject.get("poster_path")?.asString ?: "", | ||||
|                 airDate = response.asJsonObject.get("air_date")?.asString ?: "", | ||||
|                 episodes = episodes, | ||||
|                 seasonNumber = response.get("season_number")?.asInt | ||||
|                 seasonNumber = response.get("season_number")?.asInt ?: -1 | ||||
|             ) | ||||
|         } catch (ex: Exception) { | ||||
|             Log.w(javaClass.name, "Error", ex) | ||||
|             TVSeason(-1) | ||||
|             null | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -32,19 +32,19 @@ data class TVShow( | ||||
|  | ||||
| data class TVSeason( | ||||
|     val id: Int, | ||||
|     val name: String? = null, | ||||
|     val overview: String? = null, | ||||
|     val posterPath: String? = null, | ||||
|     val airDate: String? = null, | ||||
|     val episodes: List<TVEpisode>? = null, | ||||
|     val seasonNumber: Int? = null | ||||
|     val name: String, | ||||
|     val overview: String, | ||||
|     val posterPath: String, | ||||
|     val airDate: String, | ||||
|     val episodes: List<TVEpisode>, | ||||
|     val seasonNumber: Int | ||||
| ) | ||||
|  | ||||
| // TODO decide whether to use nullable or not | ||||
| data class TVEpisode( | ||||
|     val id: Int, | ||||
|     val name: String? = null, | ||||
|     val overview: String? = null, | ||||
|     val airDate: String? = null, | ||||
|     val episodeNumber: Int? = null | ||||
|     val name: String, | ||||
|     val overview: String, | ||||
|     val airDate: String, | ||||
|     val episodeNumber: Int | ||||
| ) | ||||
		Reference in New Issue
	
	Block a user