diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt index b6e1502..69905a5 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt @@ -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) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt index 3a0010f..3d9e13e 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt @@ -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 diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt index 78f480f..2a8b0fa 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentEpisodes.kt @@ -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 diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt index d155f31..e897f68 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt @@ -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) { diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 5dcd69f..c34d321 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -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 diff --git a/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt new file mode 100644 index 0000000..84564e0 --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt @@ -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() + + @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 +) + +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 +): 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 +) diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt index 6eb467c..a131a3b 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt @@ -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) : RecyclerView.Adapter() { +class EpisodeItemAdapter(private val episodes: List, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { var onImageClick: ((String, Int) -> Unit)? = null @@ -32,7 +33,13 @@ class EpisodeItemAdapter(private val episodes: List) : 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) diff --git a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt index e68250e..e5c4348 100644 --- a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt @@ -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 } } diff --git a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt index 608831a..08fdc70 100644 --- a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt @@ -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? = null, - val seasonNumber: Int? = null + val name: String, + val overview: String, + val posterPath: String, + val airDate: String, + val episodes: List, + 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 ) \ No newline at end of file