From 44f99295e9addf657e06ac5d392cebfa9f00b615 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 10 Jul 2021 23:37:16 +0200 Subject: [PATCH 01/14] rework the tmdb controller the tmdb interation now provides additional information: * tv seasons & episodes * movie & tv show (air date, status) --- .../activity/main/fragments/MediaFragment.kt | 23 ++- .../main/viewmodel/MediaFragmentViewModel.kt | 62 +++++++- .../java/org/mosad/teapod/util/DataTypes.kt | 12 -- .../mosad/teapod/util/TMDBApiController.kt | 121 --------------- .../teapod/util/tmdb/TMDBApiController.kt | 139 ++++++++++++++++++ .../mosad/teapod/util/tmdb/TMDBDataTypes.kt | 50 +++++++ 6 files changed, 259 insertions(+), 148 deletions(-) delete mode 100644 app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt create mode 100644 app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt create mode 100644 app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt 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 a762032..3a0010f 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 @@ -25,6 +25,8 @@ import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.Episode import org.mosad.teapod.util.StorageController +import org.mosad.teapod.util.tmdb.Movie +import org.mosad.teapod.util.tmdb.TMDBApiController /** * The media detail fragment. @@ -85,21 +87,25 @@ class MediaFragment(private val mediaId: Int) : Fragment() { */ private fun updateGUI() = with(model) { // generic gui - val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl - val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl + val backdropUrl = tmdbResult.backdropPath?.let { TMDBApiController.imageUrl + it } + ?: media.info.posterUrl + val posterUrl = tmdbResult.posterPath?.let { TMDBApiController.imageUrl + it } + ?: media.info.posterUrl + // load poster and backdrop + Glide.with(requireContext()).load(posterUrl) + .into(binding.imagePoster) Glide.with(requireContext()).load(backdropUrl) .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) .into(binding.imageBackdrop) - Glide.with(requireContext()).load(posterUrl) - .into(binding.imagePoster) - binding.textTitle.text = media.info.title binding.textYear.text = media.info.year.toString() binding.textAge.text = media.info.age.toString() binding.textOverview.text = media.info.shortDesc + + // set "my list" indicator if (StorageController.myList.contains(media.id)) { Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) } else { @@ -133,12 +139,13 @@ class MediaFragment(private val mediaId: Int) : Fragment() { fragments.add(MediaFragmentEpisodes()) pagerAdapter.notifyDataSetChanged() } else if (media.type == MediaType.MOVIE) { + val tmdbMovie = (tmdbResult as Movie) - if (tmdb.runtime > 0) { + if (tmdbMovie.runtime != null) { binding.textEpisodesOrRuntime.text = resources.getQuantityString( R.plurals.text_runtime, - tmdb.runtime, - tmdb.runtime + tmdbMovie.runtime, + tmdbMovie.runtime ) } else { binding.textEpisodesOrRuntime.visibility = View.GONE 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 c2ba21d..d155f31 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 @@ -1,10 +1,14 @@ package org.mosad.teapod.ui.activity.main.viewmodel import android.app.Application +import android.util.Log import androidx.lifecycle.AndroidViewModel import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.util.* 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 /** * handle media, next ep and tmdb @@ -15,7 +19,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic internal set var nextEpisode = Episode() internal set - var tmdb = TMDBResponse() + lateinit var tmdbResult: TMDBResult // TODO rename internal set /** @@ -23,14 +27,32 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic */ suspend fun load(mediaId: Int) { media = AoDParser.getMediaById(mediaId) - tmdb = TMDBApiController().search(media.info.title, media.type) + + val tmdbApiController = TMDBApiController() + val searchTitle = stripTitleInfo(media.info.title) + val tmdbId = tmdbApiController.search(searchTitle, media.type) + + tmdbResult = when (media.type) { + MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) + MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) + else -> Movie(-1) + } + 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.") + } + + // TESTING END if (media.type == MediaType.TVSHOW) { - nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { - media.episodes.first{ !it.watched } - } else { - media.episodes.first() - } + nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() } } @@ -45,4 +67,30 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic ?: media.episodes.first() } + // remove unneeded info from the media title before searching + private fun stripTitleInfo(title: String): String { + return title.replace("(Sub)", "") + .replace(Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)"), "") + .replace(Regex("(Staffel|Season)\\s?[0-9]+"), "") + .trim() + } + + /** guess Season from title + * if the title ends with a number, that could be the season + * if the title ends with Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)") or + * Regex("(Staffel|Season)\\s?[0-9]+"), that is the season information + */ + private fun guessSeasonFromTitle(title: String): Int { + val helpTitle = title.replace("(Sub)", "").trim() + Log.d("test", "helpTitle: $helpTitle") + + return if (helpTitle.last().isDigit()) { + helpTitle.last().digitToInt() + } else { + Regex("([0-9]+.\\s?(Staffel|Season))|((Staffel|Season)\\s?[0-9]+)") + .find(helpTitle) + ?.value?.filter { it.isDigit() }?.toInt() ?: 1 + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index 56635e5..8ea34dd 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -98,18 +98,6 @@ data class Stream( val language : Locale ) -/** - * this class is used for tmdb responses - */ -data class TMDBResponse( - val id: Int = 0, - val title: String = "", - val overview: String = "", - val posterUrl: String = "", - val backdropUrl: String = "", - val runtime: Int = 0 -) - /** * this class is used to represent the aod json API? */ diff --git a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt deleted file mode 100644 index 846d544..0000000 --- a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt +++ /dev/null @@ -1,121 +0,0 @@ -package org.mosad.teapod.util - -import android.util.Log -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import kotlinx.coroutines.* -import org.mosad.teapod.util.DataTypes.MediaType -import java.net.URL -import java.net.URLEncoder - -class TMDBApiController { - - private val apiUrl = "https://api.themoviedb.org/3" - private val searchMovieUrl = "$apiUrl/search/movie" - private val searchTVUrl = "$apiUrl/search/tv" - private val getMovieUrl = "$apiUrl/movie" - private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2" - private val language = "de" - private val preparedParameters = "?api_key=$apiKey&language=$language" - - private val imageUrl = "https://image.tmdb.org/t/p/w500" - - suspend fun search(title: String, type: MediaType): TMDBResponse { - // remove unneeded text from the media title before searching - val searchTerm = title.replace("(Sub)", "") - .replace(Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)"), "") - .replace(Regex("(Staffel|Season)\\s?[0-9]+"), "") - .trim() - - return when (type) { - MediaType.MOVIE -> searchMovie(searchTerm) - MediaType.TVSHOW -> searchTVShow(searchTerm) - else -> { - Log.e(javaClass.name, "Wrong Type: $type") - TMDBResponse() - } - } - - } - - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) { - val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") - val response = JsonParser.parseString(url.readText()).asJsonObject -// println(response) - - val sortedResults = response.get("results").asJsonArray.toList().sortedBy { - getStringNotNull(it.asJsonObject, "name") - } - - return@withContext if (sortedResults.isNotEmpty()) { - sortedResults.first().asJsonObject.let { - val id = getStringNotNull(it, "id").toInt() - val overview = getStringNotNull(it, "overview") - val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) - val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - - TMDBResponse(id, "", overview, posterPath, backdropPath) - } - } else { - TMDBResponse() - } - } - - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) { - val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") - val response = JsonParser.parseString(url.readText()).asJsonObject -// println(response) - - val sortedResults = response.get("results").asJsonArray.toList().sortedBy { - getStringNotNull(it.asJsonObject, "title") - } - - return@withContext if (sortedResults.isNotEmpty()) { - sortedResults.first().asJsonObject.let { - val id = getStringNotNull(it,"id").toInt() - val overview = getStringNotNull(it,"overview") - val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) - val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - val runtime = getMovieRuntime(id) - - TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) - } - } else { - TMDBResponse() - } - } - - /** - * currently only used for runtime, need a rework - */ - @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getMovieRuntime(id: Int): Int = withContext(Dispatchers.IO) { - val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language") - - val response = JsonParser.parseString(url.readText()).asJsonObject - return@withContext getStringNotNull(response,"runtime").toInt() - } - - /** - * return memberName as string if it's not JsonNull, - * else return an empty string - */ - private fun getStringNotNull(jsonObject: JsonObject, memberName: String): String { - return getStringNotNullPrefix(jsonObject, memberName, "") - } - - /** - * return memberName as string with a prefix if it's not JsonNull, - * else return an empty string - */ - private fun getStringNotNullPrefix(jsonObject: JsonObject, memberName: String, prefix: String): String { - return if (!jsonObject.get(memberName).isJsonNull) { - prefix + jsonObject.get(memberName).asString - } else { - "" - } - } - -} \ No newline at end of file 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 new file mode 100644 index 0000000..e68250e --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt @@ -0,0 +1,139 @@ +package org.mosad.teapod.util.tmdb + +import android.util.Log +import com.google.gson.JsonParser +import kotlinx.coroutines.* +import org.mosad.teapod.util.DataTypes.MediaType +import java.io.FileNotFoundException +import java.net.URL +import java.net.URLEncoder + +// TODO use Klaxon? +class TMDBApiController { + + 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") + 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 + } + } + + 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.first().asJsonObject?.get("id")?.asInt ?: -1 + } + + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun getMovieDetails(movieId: Int): Movie = withContext(Dispatchers.IO) { + val url = URL("$detailsMovieUrl/$movieId?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 Movie(-1) + } + + return@withContext try { + Movie( + id = response.get("id").asInt, + name = response.get("title")?.asString, + overview = response.get("overview")?.asString, + posterPath = response.get("poster_path")?.asString, + backdropPath = response.get("backdrop_path")?.asString, + releaseDate = response.get("release_date")?.asString, + runtime = response.get("runtime")?.asInt + ) + } catch (ex: Exception) { + Log.w(javaClass.name, "Error", ex) + Movie(-1) + } + } + + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun getTVShowDetails(tvId: Int): TVShow = withContext(Dispatchers.IO) { + val url = URL("$detailsTVUrl/$tvId?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 TVShow(-1) + } + + return@withContext try { + TVShow( + id = response.get("id").asInt, + name = response.get("name")?.asString, + overview = response.get("overview")?.asString, + posterPath = response.get("poster_path")?.asString, + backdropPath = response.get("backdrop_path")?.asString, + firstAirDate = response.get("first_air_date")?.asString, + status = response.get("status")?.asString + ) + } catch (ex: Exception) { + Log.w(javaClass.name, "Error", ex) + TVShow(-1) + } + } + + @Suppress("BlockingMethodInNonBlockingContext") + 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) + } + // println(response) + + return@withContext try { + 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 + ) + } + + 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, + episodes = episodes, + seasonNumber = response.get("season_number")?.asInt + ) + } catch (ex: Exception) { + Log.w(javaClass.name, "Error", ex) + TVSeason(-1) + } + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..608831a --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt @@ -0,0 +1,50 @@ +package org.mosad.teapod.util.tmdb + +abstract class TMDBResult{ + abstract val id: Int + abstract val name: String? + abstract val overview: String? + abstract val posterPath: String? + abstract val backdropPath: String? +} + +data class Movie( + override val id: Int, + override val name: String? = null, + override val overview: String? = null, + override val posterPath: String? = null, + override val backdropPath: String? = null, + val releaseDate: String? = null, + val runtime: Int? = null + // TODO generes +): TMDBResult() + +data class TVShow( + override val id: Int, + override val name: String? = null, + override val overview: String? = null, + override val posterPath: String? = null, + override val backdropPath: String? = null, + val firstAirDate: String? = null, + val status: String? = null, + // TODO generes +): TMDBResult() + +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 +) + +// 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 +) \ No newline at end of file -- 2.44.0 From c66c725ee342856b7f7b012b4fbd8ddeeaed96db Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 11 Jul 2021 12:56:21 +0200 Subject: [PATCH 02/14] use tmdb data if missing on aod * episode description --- .../teapod/ui/activity/main/MainActivity.kt | 7 +- .../activity/main/fragments/MediaFragment.kt | 1 - .../main/fragments/MediaFragmentEpisodes.kt | 2 +- .../main/viewmodel/MediaFragmentViewModel.kt | 57 +++++++-- .../ui/activity/player/PlayerViewModel.kt | 15 ++- .../org/mosad/teapod/util/MetaDBController.kt | 119 ++++++++++++++++++ .../teapod/util/adapter/EpisodeItemAdapter.kt | 11 +- .../teapod/util/tmdb/TMDBApiController.kt | 24 ++-- .../mosad/teapod/util/tmdb/TMDBDataTypes.kt | 20 +-- 9 files changed, 215 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/org/mosad/teapod/util/MetaDBController.kt 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 -- 2.44.0 From 26d2da923b445b35fd8d0e4d53a7251915223e2b Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 17 Jul 2021 13:15:14 +0200 Subject: [PATCH 03/14] use Gson in TMDBApiController, adapt tmdb types to api documentation * use gson fromJson() to parse tmdb response * adapt tmd types to documentation (nullable/non nullable) --- .../activity/main/fragments/MediaFragment.kt | 8 +- .../main/viewmodel/MediaFragmentViewModel.kt | 5 +- .../org/mosad/teapod/util/MetaDBController.kt | 49 ++++++- .../teapod/util/tmdb/TMDBApiController.kt | 138 +++++++++--------- .../mosad/teapod/util/tmdb/TMDBDataTypes.kt | 74 ++++++++-- 5 files changed, 176 insertions(+), 98 deletions(-) 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 3d9e13e..6aa1742 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 @@ -86,9 +86,9 @@ class MediaFragment(private val mediaId: Int) : Fragment() { */ private fun updateGUI() = with(model) { // generic gui - val backdropUrl = tmdbResult.backdropPath?.let { TMDBApiController.imageUrl + it } + val backdropUrl = tmdbResult?.backdropPath?.let { TMDBApiController.imageUrl + it } ?: media.info.posterUrl - val posterUrl = tmdbResult.posterPath?.let { TMDBApiController.imageUrl + it } + val posterUrl = tmdbResult?.posterPath?.let { TMDBApiController.imageUrl + it } ?: media.info.posterUrl // load poster and backdrop @@ -138,9 +138,9 @@ class MediaFragment(private val mediaId: Int) : Fragment() { fragments.add(MediaFragmentEpisodes()) pagerAdapter.notifyDataSetChanged() } else if (media.type == MediaType.MOVIE) { - val tmdbMovie = (tmdbResult as Movie) + val tmdbMovie = (tmdbResult as Movie?) - if (tmdbMovie.runtime != null) { + if (tmdbMovie?.runtime != null) { binding.textEpisodesOrRuntime.text = resources.getQuantityString( R.plurals.text_runtime, tmdbMovie.runtime, 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 e897f68..207218b 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 @@ -6,7 +6,6 @@ import androidx.lifecycle.AndroidViewModel import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.util.* 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 @@ -21,7 +20,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic internal set var nextEpisode = Episode() internal set - lateinit var tmdbResult: TMDBResult // TODO rename + var tmdbResult: TMDBResult? = null // TODO rename internal set var tmdbTVSeason: TVSeason? =null internal set @@ -56,7 +55,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic tmdbResult = when (media.type) { MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) - else -> Movie(-1) + else -> null } println(tmdbResult) // TODO diff --git a/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt index 84564e0..9a13e43 100644 --- a/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt +++ b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt @@ -1,5 +1,28 @@ +/** + * Teapod + * + * Copyright 2020-2021 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package org.mosad.teapod.util +import android.util.Log import com.google.gson.Gson import com.google.gson.annotations.SerializedName import kotlinx.coroutines.* @@ -25,20 +48,30 @@ class MetaDBController { } } + /** + * Get the meta data for a movie from MetaDB + * @param aodId The AoD id of the media + * @return A meta movie object, or null if not found + */ suspend fun getMovieMetadata(aodId: Int): MovieMeta? { return metaCacheList.firstOrNull { it.aodId == aodId - } as MovieMeta? ?: getMovieMetadata2(aodId) + } as MovieMeta? ?: getMovieMetadataFromDB(aodId) } + /** + * Get the meta data for a tv show from MetaDB + * @param aodId The AoD id of the media + * @return A meta tv show object, or null if not found + */ suspend fun getTVShowMetadata(aodId: Int): TVShowMeta? { return metaCacheList.firstOrNull { it.aodId == aodId - } as TVShowMeta? ?: getTVShowMetadata2(aodId) + } as TVShowMeta? ?: getTVShowMetadataFromDB(aodId) } @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun getMovieMetadata2(aodId: Int): MovieMeta? = withContext(Dispatchers.IO) { + private suspend fun getMovieMetadataFromDB(aodId: Int): MovieMeta? = withContext(Dispatchers.IO) { val url = URL("$repoUrl/movie/$aodId/media.json") return@withContext try { val json = url.readText() @@ -47,12 +80,13 @@ class MetaDBController { meta } catch (ex: FileNotFoundException) { + Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex) null } } @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun getTVShowMetadata2(aodId: Int): TVShowMeta? = withContext(Dispatchers.IO) { + private suspend fun getTVShowMetadataFromDB(aodId: Int): TVShowMeta? = withContext(Dispatchers.IO) { val url = URL("$repoUrl/tv/$aodId/media.json") return@withContext try { val json = url.readText() @@ -61,23 +95,26 @@ class MetaDBController { meta } catch (ex: FileNotFoundException) { + Log.w(javaClass.name, "Waring: The requested file was not found. Requested ID: $aodId", ex) null } } } -// TODO move data classes +// class representing the media list json object data class MediaList( val media: List ) +// abstract class used for meta data objects (tv, movie) abstract class Meta { abstract val id: Int abstract val aodId: Int abstract val tmdbId: Int } +// class representing the movie json object data class MovieMeta( override val id: Int, @SerializedName("aod_id") @@ -86,6 +123,7 @@ data class MovieMeta( override val tmdbId: Int ): Meta() +// class representing the tv show json object data class TVShowMeta( override val id: Int, @SerializedName("aod_id") @@ -100,6 +138,7 @@ data class TVShowMeta( val episodes: List ): Meta() +// class used in TVShowMeta, part of the tv show json object data class EpisodeMeta( val id: Int, @SerializedName("aod_media_id") 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 e5c4348..067a496 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 @@ -1,6 +1,29 @@ +/** + * Teapod + * + * Copyright 2020-2021 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package org.mosad.teapod.util.tmdb import android.util.Log +import com.google.gson.Gson import com.google.gson.JsonParser import kotlinx.coroutines.* import org.mosad.teapod.util.DataTypes.MediaType @@ -8,7 +31,13 @@ import java.io.FileNotFoundException import java.net.URL import java.net.URLEncoder -// TODO use Klaxon? +/** + * 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 apiUrl = "https://api.themoviedb.org/3" @@ -25,6 +54,12 @@ class TMDBApiController { } @Suppress("BlockingMethodInNonBlockingContext") + /** + * 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 + */ suspend fun search(query: String, type: MediaType): Int = withContext(Dispatchers.IO) { val searchUrl = when (type) { MediaType.MOVIE -> searchMovieUrl @@ -45,93 +80,56 @@ class TMDBApiController { } @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getMovieDetails(movieId: Int): Movie = withContext(Dispatchers.IO) { + /** + * 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 + */ + suspend fun getMovieDetails(movieId: Int): Movie? = withContext(Dispatchers.IO) { val url = URL("$detailsMovieUrl/$movieId?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 Movie(-1) - } - return@withContext try { - Movie( - id = response.get("id").asInt, - name = response.get("title")?.asString, - overview = response.get("overview")?.asString, - posterPath = response.get("poster_path")?.asString, - backdropPath = response.get("backdrop_path")?.asString, - releaseDate = response.get("release_date")?.asString, - runtime = response.get("runtime")?.asInt - ) - } catch (ex: Exception) { - Log.w(javaClass.name, "Error", ex) - Movie(-1) + val json = url.readText() + Gson().fromJson(json, Movie::class.java) + } catch (ex: FileNotFoundException) { + Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $movieId", ex) + null } } @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getTVShowDetails(tvId: Int): TVShow = withContext(Dispatchers.IO) { + /** + * 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 + */ + suspend fun getTVShowDetails(tvId: Int): TVShow? = withContext(Dispatchers.IO) { val url = URL("$detailsTVUrl/$tvId?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 TVShow(-1) - } - return@withContext try { - TVShow( - id = response.get("id").asInt, - name = response.get("name")?.asString, - overview = response.get("overview")?.asString, - posterPath = response.get("poster_path")?.asString, - backdropPath = response.get("backdrop_path")?.asString, - firstAirDate = response.get("first_air_date")?.asString, - status = response.get("status")?.asString - ) - } catch (ex: Exception) { - Log.w(javaClass.name, "Error", ex) - TVShow(-1) + val json = url.readText() + Gson().fromJson(json, TVShow::class.java) + } catch (ex: FileNotFoundException) { + Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $tvId", ex) + null } } @Suppress("BlockingMethodInNonBlockingContext") + /** + * 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 + */ 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 null - } - // println(response) - return@withContext try { - 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 ?: -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 ?: "", - episodes = episodes, - seasonNumber = response.get("season_number")?.asInt ?: -1 - ) - } catch (ex: Exception) { - Log.w(javaClass.name, "Error", ex) + val json = url.readText() + Gson().fromJson(json, TVSeason::class.java) + } catch (ex: FileNotFoundException) { + Log.w(javaClass.name, "Waring: The requested media was not found. Requested ID: $tvId, Season: $seasonNumber", ex) 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 08fdc70..5a474c7 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 @@ -1,32 +1,69 @@ +/** + * Teapod + * + * Copyright 2020-2021 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + package org.mosad.teapod.util.tmdb +import com.google.gson.annotations.SerializedName + +/** + * 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? + abstract val name: String + abstract val overview: String? // for movies tmdb return string or null abstract val posterPath: String? abstract val backdropPath: String? } data class Movie( override val id: Int, - override val name: String? = null, - override val overview: String? = null, - override val posterPath: String? = null, - override val backdropPath: String? = null, - val releaseDate: String? = null, - val runtime: Int? = null + 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() data class TVShow( override val id: Int, - override val name: String? = null, - override val overview: String? = null, - override val posterPath: String? = null, - override val backdropPath: String? = null, - val firstAirDate: String? = null, - val status: String? = null, + 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, // TODO generes ): TMDBResult() @@ -34,17 +71,22 @@ data class TVSeason( val id: Int, val name: String, val overview: String, - val posterPath: String, + @SerializedName("poster_path") + val posterPath: String?, + @SerializedName("air_date") val airDate: String, + @SerializedName("episodes") val episodes: List, + @SerializedName("season_number") val seasonNumber: Int ) -// TODO decide whether to use nullable or not data class TVEpisode( val id: Int, val name: String, val overview: String, + @SerializedName("air_date") val airDate: String, + @SerializedName("episode_number") val episodeNumber: Int ) \ No newline at end of file -- 2.44.0 From 9dfd2cf70b9072f98e729cfc77f26e5ec684d76f Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 17 Jul 2021 19:40:16 +0200 Subject: [PATCH 04/14] added skip opening for tv shows * available for tv shows, where metaDB has the needed information --- .../ui/activity/player/PlayerActivity.kt | 68 ++++++++++++++++--- .../ui/activity/player/PlayerViewModel.kt | 18 ++++- .../org/mosad/teapod/util/MetaDBController.kt | 10 ++- app/src/main/res/layout/activity_player.xml | 16 +++++ app/src/main/res/values-de-rDE/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 97 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index 66728f4..24682f5 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -32,10 +32,7 @@ import org.mosad.teapod.R import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.ui.components.EpisodesListPlayer import org.mosad.teapod.ui.components.LanguageSettingsPlayer -import org.mosad.teapod.util.DataTypes -import org.mosad.teapod.util.hideBars -import org.mosad.teapod.util.isInPiPMode -import org.mosad.teapod.util.navToLauncherTask +import org.mosad.teapod.util.* import java.util.* import java.util.concurrent.TimeUnit import kotlin.concurrent.scheduleAtFixedRate @@ -226,7 +223,10 @@ class PlayerActivity : AppCompatActivity() { // when the player controls get hidden, hide the bars too video_view.setControllerVisibilityListener { when (it) { - View.GONE -> hideBars() + View.GONE -> { + hideBars() + // TODO also hide the skip op button + } View.VISIBLE -> updateControls() } } @@ -244,6 +244,7 @@ class PlayerActivity : AppCompatActivity() { rwd_10.setOnButtonClickListener { rewind() } ffwd_10.setOnButtonClickListener { fastForward() } button_next_ep.setOnClickListener { playNextEpisode() } + button_skip_op.setOnClickListener { skipOpening() } button_language.setOnClickListener { showLanguageSettings() } button_episodes.setOnClickListener { showEpisodesList() } button_next_ep_c.setOnClickListener { playNextEpisode() } @@ -262,16 +263,20 @@ class PlayerActivity : AppCompatActivity() { timerUpdates = Timer().scheduleAtFixedRate(0, 500) { lifecycleScope.launch { + val currentPosition = model.player.currentPosition val btnNextEpIsVisible = button_next_ep.isVisible val controlsVisible = controller.isVisible + // make sure remaining time is > 0 if (model.player.duration > 0) { - remainingTime = model.player.duration - model.player.currentPosition + remainingTime = model.player.duration - currentPosition remainingTime = if (remainingTime < 0) 0 else remainingTime } + // TODO add metaDB ending_start support + // if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip: + // show next ep button if (remainingTime in 1..20000) { - // if the next ep button is not visible, make it visible. Don't show in pip mode if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) { showButtonNextEp() } @@ -279,6 +284,19 @@ class PlayerActivity : AppCompatActivity() { hideButtonNextEp() } + // if meta data is present and opening_start & opening_duration are valid, show skip opening + model.currentEpisodeMeta?.let { + if (it.openingDuration > 0 && + currentPosition in it.openingStart..(it.openingStart + 10000) && + !button_skip_op.isVisible + ) { + showButtonSkipOp() + } else if (button_skip_op.isVisible && currentPosition !in it.openingStart..(it.openingStart + 10000)) { + // the button should only be visible, if currentEpisodeMeta != null + hideButtonSkipOp() + } + } + // if controls are visible, update them if (controlsVisible) { updateControls() @@ -376,12 +394,21 @@ class PlayerActivity : AppCompatActivity() { hideButtonNextEp() } + private fun skipOpening() { + // calculate the seek time + model.currentEpisodeMeta?.let { + val seekTime = (it.openingStart + it.openingDuration) - model.player.currentPosition + model.seekToOffset(seekTime) + } + + } + /** * show the next episode button * TODO improve the show animation */ private fun showButtonNextEp() { - button_next_ep.visibility = View.VISIBLE + button_next_ep.isVisible = true button_next_ep.alpha = 0.0f button_next_ep.animate() @@ -399,7 +426,28 @@ class PlayerActivity : AppCompatActivity() { .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) - button_next_ep.visibility = View.GONE + button_next_ep.isVisible = false + } + }) + + } + + private fun showButtonSkipOp() { + button_skip_op.isVisible = true + button_skip_op.alpha = 0.0f + + button_skip_op.animate() + .alpha(1.0f) + .setListener(null) + } + + private fun hideButtonSkipOp() { + button_skip_op.animate() + .alpha(0.0f) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + super.onAnimationEnd(animation) + button_skip_op.isVisible = false } }) @@ -437,7 +485,7 @@ class PlayerActivity : AppCompatActivity() { */ override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { if (!isInPiPMode()) { - if (controller.isVisible) controller.hide() else controller.show() + if (controller.isVisible) controller.hide() else controller.show() } return true 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 c34d321..2894e21 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 @@ -27,6 +27,9 @@ import kotlin.collections.ArrayList * PlayerViewModel handles all stuff related to media/episodes. * When currentEpisode is changed the player will start playing it (not initial media), * the next episode will be update and the callback is handled. + * + * TODO rework don't use episodes for everything, use media instead + * this is a major rework of the AoDParser/Player/Media architecture */ class PlayerViewModel(application: Application) : AndroidViewModel(application) { @@ -45,6 +48,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) internal set var mediaMeta: Meta? = null internal set + var currentEpisodeMeta: EpisodeMeta? = null + internal set var currentLanguage: Locale = Locale.ROOT internal set @@ -75,11 +80,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) fun loadMedia(mediaId: Int, episodeId: Int) { runBlocking { media = AoDParser.getMediaById(mediaId) - mediaMeta = loadMediaMeta(media.id) + mediaMeta = loadMediaMeta(media.id) // can be done blocking, since it should be cached } currentEpisode = media.getEpisodeById(episodeId) nextEpisode = selectNextEpisode() + currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.id) currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language } @@ -121,6 +127,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) currentLanguage = preferredStream.language // update current language, since it may have changed currentEpisode = episode nextEpisode = selectNextEpisode() + currentEpisodeMeta = getEpisodeMetaByAoDMediaId(episode.id) currentEpisodeChangedListener.forEach { it() } // update player gui (title) val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( @@ -160,6 +167,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) } } + fun getEpisodeMetaByAoDMediaId(aodMediaId: Int): EpisodeMeta? { + val meta = mediaMeta + return if (meta is TVShowMeta) { + meta.episodes.firstOrNull { it.aodMediaId == aodMediaId } + } else { + null + } + } + private suspend fun loadMediaMeta(aodId: Int): Meta? { return if (media.type == DataTypes.MediaType.TVSHOW) { MetaDBController().getTVShowMetadata(aodId) diff --git a/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt index 9a13e43..387a129 100644 --- a/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt +++ b/app/src/main/java/org/mosad/teapod/util/MetaDBController.kt @@ -42,8 +42,6 @@ class MetaDBController { val url = URL("$repoUrl/list.json") val json = url.readText() - Thread.sleep(5000) - mediaList = Gson().fromJson(json, MediaList::class.java) } } @@ -148,11 +146,11 @@ data class EpisodeMeta( @SerializedName("tmdb_number") val tmdbNumber: Int, @SerializedName("opening_start") - val openingStart: Int, + val openingStart: Long, @SerializedName("opening_duration") - val openingDuration: Int, + val openingDuration: Long, @SerializedName("ending_start") - val endingStart: Int, + val endingStart: Long, @SerializedName("ending_duration") - val endingDuration: Int + val endingDuration: Long ) diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml index 5a8f6bd..566f92f 100644 --- a/app/src/main/res/layout/activity_player.xml +++ b/app/src/main/res/layout/activity_player.xml @@ -89,4 +89,20 @@ app:backgroundTint="@color/exo_white" app:iconGravity="textStart" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index af2731e..a957051 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -69,6 +69,7 @@ Abspielen/Pause 10 Sekunden vorwärts Nächste Folge + Intro überspringen Sprache Folgen Folge diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7ba0b9..a58f3a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ - 10 s + 10 s Next Episode + Skip Opening %1$02d:%2$02d %1$d:%2$02d:%3$02d Language -- 2.44.0 From 0340c83b47a46bb4fc5ab703a38cdc9cfe101a61 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 25 Jul 2021 19:15:31 +0200 Subject: [PATCH 05/14] clean up some AoDParser related code --- app/build.gradle | 2 +- .../java/org/mosad/teapod/parser/AoDParser.kt | 62 ++++++++++++------- .../activity/main/fragments/HomeFragment.kt | 22 +++---- .../main/fragments/LibraryFragment.kt | 2 +- .../activity/main/fragments/MediaFragment.kt | 6 +- .../activity/main/fragments/SearchFragment.kt | 2 +- .../main/viewmodel/MediaFragmentViewModel.kt | 22 +------ .../java/org/mosad/teapod/util/DataTypes.kt | 8 +-- .../teapod/util/adapter/EpisodeItemAdapter.kt | 4 +- .../teapod/util/tmdb/TMDBApiController.kt | 12 ++-- .../mosad/teapod/util/tmdb/TMDBDataTypes.kt | 10 +-- 11 files changed, 76 insertions(+), 76 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 76818ad..7e5ec68 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdkVersion 23 targetSdkVersion 30 versionCode 4200 //00.04.200 - versionName "0.4.2" + versionName "0.5.0-alpha1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index d68a8fb..4481800 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -49,7 +49,10 @@ object AoDParser { private var loginSuccess = false private val mediaList = arrayListOf() // actual media (data) - val itemMediaList = arrayListOf() // gui media + private val aodMediaList = arrayListOf() + + // gui media + val guiMediaList = arrayListOf() val highlightsList = arrayListOf() val newEpisodesList = arrayListOf() val newSimulcastsList = arrayListOf() @@ -110,8 +113,8 @@ object AoDParser { * get a media by it's ID (int) * @return Media */ - suspend fun getMediaById(mediaId: Int): Media { - val media = mediaList.first { it.id == mediaId } + suspend fun getMediaById(aodId: Int): Media { + val media = mediaList.first { it.id == aodId } if (media.episodes.isEmpty()) { loadStreams(media).join() @@ -180,24 +183,39 @@ object AoDParser { val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() //println(resAnimes) - itemMediaList.clear() + guiMediaList.clear() mediaList.clear() - resAnimes.select("div.animebox").forEach { - val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { - MediaType.TVSHOW - } else { - MediaType.MOVIE - } - val mediaTitle = it.select("h3.animebox-title").text() - val mediaLink = it.select("p.animebox-link").select("a").attr("href") - val mediaImage = it.select("p.animebox-image").select("img").attr("src") - val mediaShortText = it.select("p.animebox-shorttext").text() - val mediaId = mediaLink.substringAfterLast("/").toInt() + val animes = resAnimes.select("div.animebox") - itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) - mediaList.add(Media(mediaId, mediaLink, type).apply { - info.title = mediaTitle - info.posterUrl = mediaImage + guiMediaList.addAll( + animes.map { + ItemMedia( + id = it.select("p.animebox-link").select("a") + .attr("href").substringAfterLast("/").toInt(), + title = it.select("h3.animebox-title").text(), + posterUrl = it.select("p.animebox-image").select("img") + .attr("src") + ) + } + ) + + // TODO legacy + resAnimes.select("div.animebox").forEach { + val id = it.select("p.animebox-link").select("a").attr("href") + .substringAfterLast("/").toInt() + val title = it.select("h3.animebox-title").text() + val image = it.select("p.animebox-image").select("img").attr("src") + val link = it.select("p.animebox-link").select("a").attr("href") + val type = when (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT)) { + "zur serie" -> MediaType.TVSHOW + "zum film" -> MediaType.MOVIE + else -> MediaType.OTHER + } + val mediaShortText = it.select("p.animebox-shorttext").text() + + mediaList.add(Media(id, link, type).apply { + info.title = title + info.posterUrl = image info.shortDesc = mediaShortText }) } @@ -410,9 +428,9 @@ object AoDParser { /** * don't use Gson().fromJson() as we don't have any control over the api and it may change */ - private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred { + private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred { if (playlistPath == "[]") { - return CompletableDeferred(AoDObject(listOf(), language)) + return CompletableDeferred(AoDPlaylist(listOf(), language)) } return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) { @@ -435,7 +453,7 @@ object AoDParser { //Gson().fromJson(res.body(), AoDObject::class.java) - return@async AoDObject(JsonParser.parseString(res.body()).asJsonObject + return@async AoDPlaylist(JsonParser.parseString(res.body()).asJsonObject .get("playlist").asJsonArray.map { Playlist( sources = it.asJsonObject.get("sources").asJsonArray.map { source -> diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index 1f45bcc..29fed64 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -120,24 +120,24 @@ class HomeFragment : Fragment() { activity?.showFragment(MediaFragment(highlightMedia.id)) } - adapterMyList.onItemClick = { mediaId, _ -> - activity?.showFragment(MediaFragment(mediaId)) + adapterMyList.onItemClick = { id, _ -> + activity?.showFragment(MediaFragment(id)) } - adapterNewEpisodes.onItemClick = { mediaId, _ -> - activity?.showFragment(MediaFragment(mediaId)) + adapterNewEpisodes.onItemClick = { id, _ -> + activity?.showFragment(MediaFragment(id)) } - adapterNewSimulcasts.onItemClick = { mediaId, _ -> - activity?.showFragment(MediaFragment(mediaId)) + adapterNewSimulcasts.onItemClick = { id, _ -> + activity?.showFragment(MediaFragment(id)) } - adapterNewTitles.onItemClick = { mediaId, _ -> - activity?.showFragment(MediaFragment(mediaId)) + adapterNewTitles.onItemClick = { id, _ -> + activity?.showFragment(MediaFragment(id)) } - adapterTopTen.onItemClick = { mediaId, _ -> - activity?.showFragment(MediaFragment(mediaId)) + adapterTopTen.onItemClick = { id, _ -> + activity?.showFragment(MediaFragment(id)) } } @@ -154,7 +154,7 @@ class HomeFragment : Fragment() { private fun mapMyListToItemMedia(): List { return StorageController.myList.mapNotNull { elementId -> - AoDParser.itemMediaList.firstOrNull { it.id == elementId }.also { + AoDParser.guiMediaList.firstOrNull { it.id == elementId }.also { // it the my list entry wasn't found in itemMediaList Log it if (it == null) { Log.w(javaClass.name, "The element with the id $elementId was not found.") diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt index f757b7a..b761490 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt @@ -30,7 +30,7 @@ class LibraryFragment : Fragment() { lifecycleScope.launch { // create and set the adapter, needs context context?.let { - adapter = MediaItemAdapter(AoDParser.itemMediaList) + adapter = MediaItemAdapter(AoDParser.guiMediaList) adapter.onItemClick = { mediaId, _ -> activity?.showFragment(MediaFragment(mediaId)) } 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 6aa1742..026306f 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 @@ -25,7 +25,7 @@ import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.Episode import org.mosad.teapod.util.StorageController -import org.mosad.teapod.util.tmdb.Movie +import org.mosad.teapod.util.tmdb.TMDBMovie import org.mosad.teapod.util.tmdb.TMDBApiController /** @@ -138,7 +138,7 @@ class MediaFragment(private val mediaId: Int) : Fragment() { fragments.add(MediaFragmentEpisodes()) pagerAdapter.notifyDataSetChanged() } else if (media.type == MediaType.MOVIE) { - val tmdbMovie = (tmdbResult as Movie?) + val tmdbMovie = (tmdbResult as TMDBMovie?) if (tmdbMovie?.runtime != null) { binding.textEpisodesOrRuntime.text = resources.getQuantityString( @@ -171,7 +171,7 @@ class MediaFragment(private val mediaId: Int) : Fragment() { when (media.type) { MediaType.MOVIE -> playEpisode(media.episodes.first()) MediaType.TVSHOW -> playEpisode(nextEpisode) - else -> Log.e(javaClass.name, "Wrong Type: $media.type") + else -> Log.e(javaClass.name, "Wrong Type: ${media.type}") } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt index b430092..a2943a9 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt @@ -30,7 +30,7 @@ class SearchFragment : Fragment() { lifecycleScope.launch { // create and set the adapter, needs context context?.let { - adapter = MediaItemAdapter(AoDParser.itemMediaList) + adapter = MediaItemAdapter(AoDParser.guiMediaList) adapter!!.onItemClick = { mediaId, _ -> binding.searchText.clearFocus() activity?.showFragment(MediaFragment(mediaId)) 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 207218b..6e4c724 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 @@ -8,7 +8,7 @@ import org.mosad.teapod.util.* import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.tmdb.TMDBApiController import org.mosad.teapod.util.tmdb.TMDBResult -import org.mosad.teapod.util.tmdb.TVSeason +import org.mosad.teapod.util.tmdb.TMDBTVSeason /** * handle media, next ep and tmdb @@ -22,7 +22,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic internal set var tmdbResult: TMDBResult? = null // TODO rename internal set - var tmdbTVSeason: TVSeason? =null + var tmdbTVSeason: TMDBTVSeason? =null internal set var mediaMeta: Meta? = null internal set @@ -67,24 +67,6 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic 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) { nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() } diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index 8ea34dd..397e448 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -36,7 +36,7 @@ data class ThirdPartyComponent( * it is uses in the ItemMediaAdapter (RecyclerView) */ data class ItemMedia( - val id: Int, + val id: Int, // aod path id val title: String, val posterUrl: String ) @@ -101,9 +101,9 @@ data class Stream( /** * this class is used to represent the aod json API? */ -data class AoDObject( - val playlist: List, - val extLanguage: String +data class AoDPlaylist( + val list: List, + val language: String ) data class Playlist( 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 a131a3b..89b0bf5 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,9 +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 +import org.mosad.teapod.util.tmdb.TMDBTVEpisode -class EpisodeItemAdapter(private val episodes: List, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { +class EpisodeItemAdapter(private val episodes: List, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { var onImageClick: ((String, Int) -> Unit)? = null 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 067a496..db74e8b 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 @@ -85,12 +85,12 @@ class TMDBApiController { * @param movieId The tmdb ID of the movie * @return A tmdb movie object, or null if not found */ - suspend fun getMovieDetails(movieId: Int): Movie? = withContext(Dispatchers.IO) { + suspend fun getMovieDetails(movieId: Int): TMDBMovie? = withContext(Dispatchers.IO) { val url = URL("$detailsMovieUrl/$movieId?api_key=$apiKey&language=$language") return@withContext try { val json = url.readText() - Gson().fromJson(json, Movie::class.java) + 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 @@ -103,12 +103,12 @@ class TMDBApiController { * @param tvId The tmdb ID of the tv show * @return A tmdb tv show object, or null if not found */ - suspend fun getTVShowDetails(tvId: Int): TVShow? = withContext(Dispatchers.IO) { + suspend fun getTVShowDetails(tvId: Int): TMDBTVShow? = withContext(Dispatchers.IO) { val url = URL("$detailsTVUrl/$tvId?api_key=$apiKey&language=$language") return@withContext try { val json = url.readText() - Gson().fromJson(json, TVShow::class.java) + 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 @@ -122,12 +122,12 @@ class TMDBApiController { * @param seasonNumber The tmdb season number * @return A tmdb tv season object, or null if not found */ - suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TVSeason? = withContext(Dispatchers.IO) { + suspend fun getTVSeasonDetails(tvId: Int, seasonNumber: Int): TMDBTVSeason? = withContext(Dispatchers.IO) { val url = URL("$detailsTVUrl/$tvId/season/$seasonNumber?api_key=$apiKey&language=$language") return@withContext try { val json = url.readText() - Gson().fromJson(json, TVSeason::class.java) + 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 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 5a474c7..a3f5106 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 @@ -37,7 +37,7 @@ abstract class TMDBResult{ abstract val backdropPath: String? } -data class Movie( +data class TMDBMovie( override val id: Int, override val name: String, override val overview: String?, @@ -52,7 +52,7 @@ data class Movie( // TODO generes ): TMDBResult() -data class TVShow( +data class TMDBTVShow( override val id: Int, override val name: String, override val overview: String, @@ -67,7 +67,7 @@ data class TVShow( // TODO generes ): TMDBResult() -data class TVSeason( +data class TMDBTVSeason( val id: Int, val name: String, val overview: String, @@ -76,12 +76,12 @@ data class TVSeason( @SerializedName("air_date") val airDate: String, @SerializedName("episodes") - val episodes: List, + val episodes: List, @SerializedName("season_number") val seasonNumber: Int ) -data class TVEpisode( +data class TMDBTVEpisode( val id: Int, val name: String, val overview: String, -- 2.44.0 From 309a9910075cdf7406096c5535ef6400f76d267c Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 25 Jul 2021 19:17:37 +0200 Subject: [PATCH 06/14] fix for AoDParser related code clean up --- app/src/main/java/org/mosad/teapod/parser/AoDParser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 4481800..b33291d 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -346,13 +346,13 @@ object AoDParser { playlists.forEach { aod -> // TODO improve language handling - val locale = when (aod.extLanguage) { + val locale = when (aod.language) { "ger" -> Locale.GERMAN "jap" -> Locale.JAPANESE else -> Locale.ROOT } - aod.playlist.forEach { ep -> + aod.list.forEach { ep -> try { if (media.hasEpisode(ep.mediaid)) { media.getEpisodeById(ep.mediaid).streams.add( -- 2.44.0 From d76538cf28b123f194adc1d5f48ab8dcaf89c0b9 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 25 Jul 2021 19:30:25 +0200 Subject: [PATCH 07/14] use locale instead of string for language in AoDPlaylist --- .../java/org/mosad/teapod/parser/AoDParser.kt | 21 ++++++++----------- .../java/org/mosad/teapod/util/DataTypes.kt | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index b33291d..c87e0c1 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -49,7 +49,6 @@ object AoDParser { private var loginSuccess = false private val mediaList = arrayListOf() // actual media (data) - private val aodMediaList = arrayListOf() // gui media val guiMediaList = arrayListOf() @@ -345,23 +344,16 @@ object AoDParser { }.awaitAll() playlists.forEach { aod -> - // TODO improve language handling - val locale = when (aod.language) { - "ger" -> Locale.GERMAN - "jap" -> Locale.JAPANESE - else -> Locale.ROOT - } - aod.list.forEach { ep -> try { if (media.hasEpisode(ep.mediaid)) { media.getEpisodeById(ep.mediaid).streams.add( - Stream(ep.sources.first().file, locale) + Stream(ep.sources.first().file, aod.language) ) } else { media.episodes.add(Episode( id = ep.mediaid, - streams = mutableListOf(Stream(ep.sources.first().file, locale)), + streams = mutableListOf(Stream(ep.sources.first().file, aod.language)), posterUrl = ep.image, title = ep.title, description = ep.description, @@ -430,7 +422,7 @@ object AoDParser { */ private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred { if (playlistPath == "[]") { - return CompletableDeferred(AoDPlaylist(listOf(), language)) + return CompletableDeferred(AoDPlaylist(listOf(), Locale.ROOT)) } return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) { @@ -465,7 +457,12 @@ object AoDParser { mediaid = it.asJsonObject.get("mediaid").asInt ) }, - language + // TODO improve language handling (via display language etc.) + language = when (language) { + "ger" -> Locale.GERMAN + "jap" -> Locale.JAPANESE + else -> Locale.ROOT + } ) } } diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index 397e448..449919d 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -103,7 +103,7 @@ data class Stream( */ data class AoDPlaylist( val list: List, - val language: String + val language: Locale ) data class Playlist( -- 2.44.0 From a505315781615d43b89414725a1faad720cd86ae Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 15 Aug 2021 00:11:42 +0200 Subject: [PATCH 08/14] fix crash if media is not found in tmdb --- .../main/java/org/mosad/teapod/util/tmdb/TMDBApiController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 db74e8b..93003c4 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 @@ -76,7 +76,7 @@ class TMDBApiController { it.asJsonObject.get("title")?.asString } - return@withContext sortedResults.first().asJsonObject?.get("id")?.asInt ?: -1 + return@withContext sortedResults.firstOrNull()?.asJsonObject?.get("id")?.asInt ?: -1 } @Suppress("BlockingMethodInNonBlockingContext") -- 2.44.0 From c2a5f768b89aeba5976b1c7f18bb7a4745c5f44d Mon Sep 17 00:00:00 2001 From: Jannik Date: Tue, 31 Aug 2021 19:47:18 +0200 Subject: [PATCH 09/14] AoDParser Media handling rework [Part 1/2] --- .../java/org/mosad/teapod/parser/AoDParser.kt | 150 +++++++++++++++++- .../activity/main/fragments/HomeFragment.kt | 6 +- .../activity/main/fragments/MediaFragment.kt | 63 ++++---- .../main/fragments/MediaFragmentEpisodes.kt | 17 +- .../main/fragments/MediaFragmentSimilar.kt | 2 +- .../main/viewmodel/MediaFragmentViewModel.kt | 47 ++++-- .../java/org/mosad/teapod/util/DataTypes.kt | 72 +++++++++ .../teapod/util/adapter/EpisodeItemAdapter.kt | 8 +- .../metadata/android/de/full_description.txt | 2 +- 9 files changed, 298 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index c87e0c1..95ea3a4 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -48,7 +48,8 @@ object AoDParser { private var csrfToken: String = "" private var loginSuccess = false - private val mediaList = arrayListOf() // actual media (data) + private val mediaList = arrayListOf() // actual media (data) TODO remove + private val aodMediaList = arrayListOf() // actual media (data) // gui media val guiMediaList = arrayListOf() @@ -112,16 +113,38 @@ object AoDParser { * get a media by it's ID (int) * @return Media */ + @Deprecated(message = "Use getMediaById2() instead") suspend fun getMediaById(aodId: Int): Media { val media = mediaList.first { it.id == aodId } if (media.episodes.isEmpty()) { loadStreams(media).join() + + loadMediaAsync(media.id).await() } return media } + /** + * get a media by it's ID (int) + * @param aodId The AoD ID of the requested media + * @return returns a AoDMedia of type Movie or TVShow if found, else return AoDMediaNone + */ + suspend fun getMediaById2(aodId: Int): AoDMedia { + return aodMediaList.firstOrNull { it.aodId == aodId } ?: + try { + loadMediaAsync(aodId).await().apply { + aodMediaList.add(this) + } + } catch (exn:NullPointerException) { + Log.e(javaClass.name, "Error while loading media $aodId", exn) + AoDMediaNone + } + + + } + /** * get subscription info from aod website, remove "Anime-Abo" Prefix and trim */ @@ -417,6 +440,131 @@ object AoDParser { } } + private suspend fun loadMediaAsync(aodId: Int): Deferred = coroutineScope { + return@coroutineScope async (Dispatchers.IO) { + if (sessionCookies.isEmpty()) login() // TODO is this needed? + + // return none object, if login wasn't successful + if (!loginSuccess) { + Log.w(javaClass.name, "Login, was not successful.") + return@async AoDMediaNone + } + + // get the media page + val res = Jsoup.connect("$baseUrl/anime/$aodId") + .cookies(sessionCookies) + .get() + // println(res) + + if (csrfToken.isEmpty()) { + csrfToken = res.select("meta[name=csrf-token]").attr("content") + Log.d(javaClass.name, "New csrf token is $csrfToken") + } + + // playlist parsing TODO can this be async to the genral info marsing? + val besides = res.select("div.besides").first() + val aodPlaylists = besides.select("input.streamstarter_html5").map { streamstarter -> + parsePlaylistAsync( + streamstarter.attr("data-playlist"), + streamstarter.attr("data-lang") + ) + } + + /** + * generic aod media data + */ + val title = res.select("h1[itemprop=name]").text() + val description = res.select("div[itemprop=description]").text() + val posterURL = res.select("img.fullwidth-image").attr("src") + val type = when { + posterURL.contains("films") -> MediaType.MOVIE + posterURL.contains("series") -> MediaType.TVSHOW + else -> MediaType.OTHER + } + + var year = 0 + var age = 0 + res.select("table.vertical-table").select("tr").forEach { row -> + when (row.select("th").text().lowercase(Locale.ROOT)) { + "produktionsjahr" -> year = row.select("td").text().toInt() + "fsk" -> age = row.select("td").text().toInt() + } + } + + // similar titles from media page + val similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() + + if (mediaId != null) { + ItemMedia(mediaId, mediaTitle, mediaImage) + } else { + null + } + } + + /** + * additional information for episodes: + * description: a short description of the episode + * watched: indicates if the episodes has been watched + * watched callback: url to set watched in aod + */ + val episodesInfo: Map = if (type == MediaType.TVSHOW) { + res.select("div.three-box-container > div.episodebox").mapNotNull { episodeBox -> + // make sure the episode has a streaming link + if (episodeBox.select("input.streamstarter_html5").isNotEmpty()) { + val mediaId = episodeBox.select("div.flip-front").attr("id").substringAfter("-").toInt() + val episodeShortDesc = episodeBox.select("p.episodebox-shorttext").text() + val episodeWatched = episodeBox.select("div.episodebox-icons > div").hasClass("status-icon-orange") + val episodeWatchedCallback = episodeBox.select("input.streamstarter_html5").eachAttr("data-playlist").first() + + AoDEpisodeInfo(mediaId, episodeShortDesc, episodeWatched, episodeWatchedCallback) + } else { + null + } + }.associateBy { it.aodMediaId } + } else { + mapOf() + } + + // TODO make AoDPlaylist to teapod playlist + val playlist: List = aodPlaylists.awaitAll().flatMap { aodPlaylist -> + aodPlaylist.list.mapIndexed { index, episode -> + AoDEpisode( + mediaId = episode.mediaid, + title = episode.title, + description = episode.description, + shortDesc = episodesInfo[episode.mediaid]?.shortDesc ?: "", + imageURL = episode.image, + number = index, + watched = episodesInfo[episode.mediaid]?.watched ?: false, + watchedCallback = episodesInfo[episode.mediaid]?.watchedCallback ?: "", + streams = mutableListOf(Stream(episode.sources.first().file, aodPlaylist.language)) + ) + } + }.groupingBy { it.mediaId }.reduce{ _, accumulator, element -> + accumulator.copy().also { + it.streams.addAll(element.streams) + } + }.values.toList() + println("new playlist object: $playlist") + + return@async AoDMedia( + aodId = aodId, + type = type, + title = title, + shortText = description, + posterURL = posterURL, + year = year, + age = age, + similar = similar, + playlist = playlist + ) + } + } + /** * don't use Gson().fromJson() as we don't have any control over the api and it may change */ diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index 29fed64..e9422f1 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -96,10 +96,10 @@ class HomeFragment : Fragment() { binding.buttonPlayHighlight.setOnClickListener { // TODO get next episode lifecycleScope.launch { - val media = AoDParser.getMediaById(highlightMedia.id) + val media = AoDParser.getMediaById2(highlightMedia.id) - Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}") - (activity as MainActivity).startPlayer(media.id, media.episodes.first().id) + Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}") + (activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId) } } 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 026306f..2d8cef3 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 @@ -23,7 +23,6 @@ import org.mosad.teapod.databinding.FragmentMediaBinding import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel import org.mosad.teapod.util.DataTypes.MediaType -import org.mosad.teapod.util.Episode import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.tmdb.TMDBMovie import org.mosad.teapod.util.tmdb.TMDBApiController @@ -57,7 +56,7 @@ class MediaFragment(private val mediaId: Int) : Fragment() { binding.pagerEpisodesSimilar.offscreenPageLimit = 2 binding.pagerEpisodesSimilar.adapter = pagerAdapter TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position -> - tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) { + tab.text = if (model.media2.type == MediaType.TVSHOW && position == 0) { getString(R.string.episodes) } else { getString(R.string.similar_titles) @@ -76,8 +75,9 @@ class MediaFragment(private val mediaId: Int) : Fragment() { super.onResume() // update the next ep text if there is one, since it may have changed - if (model.nextEpisode.title.isNotEmpty()) { - binding.textTitle.text = model.nextEpisode.title + println(model.nextEpisodeId) + if (model.media2.getEpisodeById(model.nextEpisodeId).title.isNotEmpty()) { + binding.textTitle.text = model.media2.getEpisodeById(model.nextEpisodeId).title } } @@ -87,9 +87,9 @@ class MediaFragment(private val mediaId: Int) : Fragment() { private fun updateGUI() = with(model) { // generic gui val backdropUrl = tmdbResult?.backdropPath?.let { TMDBApiController.imageUrl + it } - ?: media.info.posterUrl + ?: media2.posterURL val posterUrl = tmdbResult?.posterPath?.let { TMDBApiController.imageUrl + it } - ?: media.info.posterUrl + ?: media2.posterURL // load poster and backdrop Glide.with(requireContext()).load(posterUrl) @@ -99,13 +99,13 @@ class MediaFragment(private val mediaId: Int) : Fragment() { .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) .into(binding.imageBackdrop) - binding.textTitle.text = media.info.title - binding.textYear.text = media.info.year.toString() - binding.textAge.text = media.info.age.toString() - binding.textOverview.text = media.info.shortDesc + binding.textTitle.text = media2.title + binding.textYear.text = media2.year.toString() + binding.textAge.text = media2.age.toString() + binding.textOverview.text = media2.shortText // set "my list" indicator - if (StorageController.myList.contains(media.id)) { + if (StorageController.myList.contains(media2.aodId)) { Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) } else { Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) @@ -116,28 +116,25 @@ class MediaFragment(private val mediaId: Int) : Fragment() { pagerAdapter.notifyDataSetChanged() // specific gui - if (media.type == MediaType.TVSHOW) { + if (media2.type == MediaType.TVSHOW) { // get next episode - nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { - media.episodes.first{ !it.watched } - } else { - media.episodes.first() - } + nextEpisodeId = media2.playlist.firstOrNull{ !it.watched }?.mediaId + ?: media2.playlist.first().mediaId // title is the next episodes title - binding.textTitle.text = nextEpisode.title + binding.textTitle.text = media2.getEpisodeById(nextEpisodeId).title // episodes count binding.textEpisodesOrRuntime.text = resources.getQuantityString( R.plurals.text_episodes_count, - media.info.episodesCount, - media.info.episodesCount + media2.playlist.size, + media2.playlist.size ) // episodes fragments.add(MediaFragmentEpisodes()) pagerAdapter.notifyDataSetChanged() - } else if (media.type == MediaType.MOVIE) { + } else if (media2.type == MediaType.MOVIE) { val tmdbMovie = (tmdbResult as TMDBMovie?) if (tmdbMovie?.runtime != null) { @@ -152,7 +149,7 @@ class MediaFragment(private val mediaId: Int) : Fragment() { } // if has similar titles - if (media.info.similar.isNotEmpty()) { + if (media2.similar.isNotEmpty()) { fragments.add(MediaFragmentSimilar()) pagerAdapter.notifyDataSetChanged() } @@ -168,20 +165,20 @@ class MediaFragment(private val mediaId: Int) : Fragment() { private fun initActions() = with(model) { binding.buttonPlay.setOnClickListener { - when (media.type) { - MediaType.MOVIE -> playEpisode(media.episodes.first()) - MediaType.TVSHOW -> playEpisode(nextEpisode) - else -> Log.e(javaClass.name, "Wrong Type: ${media.type}") + when (media2.type) { + MediaType.MOVIE -> playEpisode(media2.playlist.first().mediaId) + MediaType.TVSHOW -> playEpisode(nextEpisodeId) + else -> Log.e(javaClass.name, "Wrong Type: ${media2.type}") } } // add or remove media from myList binding.linearMyListAction.setOnClickListener { - if (StorageController.myList.contains(media.id)) { - StorageController.myList.remove(media.id) + if (StorageController.myList.contains(media2.aodId)) { + StorageController.myList.remove(media2.aodId) Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) } else { - StorageController.myList.add(media.id) + StorageController.myList.add(media2.aodId) Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) } StorageController.saveMyList(requireContext()) @@ -197,11 +194,11 @@ class MediaFragment(private val mediaId: Int) : Fragment() { * play the current episode * TODO this is also used in MediaFragmentEpisode, we should only have on implementation */ - private fun playEpisode(ep: Episode) { - (activity as MainActivity).startPlayer(model.media.id, ep.id) - Log.d(javaClass.name, "Started Player with episodeId: ${ep.id}") + private fun playEpisode(episodeId: Int) { + (activity as MainActivity).startPlayer(model.media2.aodId, episodeId) + Log.d(javaClass.name, "Started Player with episodeId: $episodeId") - model.updateNextEpisode(ep) // set the correct next episode + model.updateNextEpisode(episodeId) // set the correct next episode } /** 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 2a8b0fa..7a0eff9 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 @@ -10,7 +10,6 @@ import androidx.fragment.app.activityViewModels import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding -import org.mosad.teapod.util.Episode import org.mosad.teapod.util.adapter.EpisodeItemAdapter class MediaFragmentEpisodes : Fragment() { @@ -28,13 +27,13 @@ class MediaFragmentEpisodes : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes, model.tmdbTVSeason?.episodes) + adapterRecEpisodes = EpisodeItemAdapter(model.media2.playlist, model.tmdbTVSeason?.episodes) binding.recyclerEpisodes.adapter = adapterRecEpisodes // set onItemClick only in adapter is initialized if (this::adapterRecEpisodes.isInitialized) { adapterRecEpisodes.onImageClick = { _, position -> - playEpisode(model.media.episodes[position]) + playEpisode(model.media2.playlist[position].mediaId) } } } @@ -44,18 +43,18 @@ class MediaFragmentEpisodes : Fragment() { // if adapterRecEpisodes is initialized, update the watched state for the episodes if (this::adapterRecEpisodes.isInitialized) { - model.media.episodes.forEachIndexed { index, episode -> - adapterRecEpisodes.updateWatchedState(episode.watched, index) + model.media2.playlist.forEachIndexed { index, episodeInfo -> + adapterRecEpisodes.updateWatchedState(episodeInfo.watched, index) } adapterRecEpisodes.notifyDataSetChanged() } } - private fun playEpisode(ep: Episode) { - (activity as MainActivity).startPlayer(model.media.id, ep.id) - Log.d(javaClass.name, "Started Player with episodeId: ${ep.id}") + private fun playEpisode(episodeId: Int) { + (activity as MainActivity).startPlayer(model.media2.aodId, episodeId) + Log.d(javaClass.name, "Started Player with episodeId: $episodeId") - model.updateNextEpisode(ep) // set the correct next episode + model.updateNextEpisode(episodeId) // set the correct next episode } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt index db6d519..dba70c3 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt @@ -27,7 +27,7 @@ class MediaFragmentSimilar : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - adapterSimilar = MediaItemAdapter(model.media.info.similar) + adapterSimilar = MediaItemAdapter(model.media2.similar) binding.recyclerMediaSimilar.adapter = adapterSimilar binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9)) 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 6e4c724..fd1f4b6 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 @@ -16,10 +16,17 @@ import org.mosad.teapod.util.tmdb.TMDBTVSeason */ class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { - var media = Media(-1, "", MediaType.OTHER) +// var media = Media(-1, "", MediaType.OTHER) +// internal set +// var nextEpisode = Episode() +// internal set + + var media2 = AoDMediaNone internal set - var nextEpisode = Episode() + var nextEpisodeId = -1 internal set + + var tmdbResult: TMDBResult? = null // TODO rename internal set var tmdbTVSeason: TMDBTVSeason? =null @@ -33,15 +40,16 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic */ suspend fun load(mediaId: Int) { val tmdbApiController = TMDBApiController() - media = AoDParser.getMediaById(mediaId) + //media = AoDParser.getMediaById(mediaId) + media2 = AoDParser.getMediaById2(mediaId) // check if metaDB knows the title - val tmdbId: Int = if (MetaDBController.mediaList.media.contains(media.id)) { + val tmdbId: Int = if (MetaDBController.mediaList.media.contains(media2.aodId)) { // load media info from metaDB val metaDB = MetaDBController() - mediaMeta = when (media.type) { - MediaType.MOVIE -> metaDB.getMovieMetadata(media.id) - MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.id) + mediaMeta = when (media2.type) { + MediaType.MOVIE -> metaDB.getMovieMetadata(media2.aodId) + MediaType.TVSHOW -> metaDB.getTVShowMetadata(media2.aodId) else -> null } @@ -49,10 +57,10 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic } 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) + tmdbApiController.search(stripTitleInfo(media2.title), media2.type) } - tmdbResult = when (media.type) { + tmdbResult = when (media2.type) { MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) else -> null @@ -60,27 +68,32 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic println(tmdbResult) // TODO // get season info, if metaDB knows the tv show - tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) { + tmdbTVSeason = if (media2.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() + if (media2.type == MediaType.TVSHOW) { + //nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() + nextEpisodeId = media2.playlist.firstOrNull { !it.watched }?.mediaId + ?: media2.playlist.first().mediaId } } /** - * get the next episode based on episode number (the true next episode) + * get the next episode based on episodeId * if no matching is found, use first episode */ - fun updateNextEpisode(currentEp: Episode) { - if (media.type == MediaType.MOVIE) return // return if movie + fun updateNextEpisode(episodeId: Int) { + if (media2.type == MediaType.MOVIE) return // return if movie - nextEpisode = media.episodes.firstOrNull{ it.number > currentEp.number } - ?: media.episodes.first() +// nextEpisode = media.episodes.firstOrNull{ it.number > currentEp.number } +// ?: media.episodes.first() + + nextEpisodeId = media2.playlist.firstOrNull { it.number > media2.getEpisodeById(episodeId).number }?.mediaId + ?: media2.playlist.first().mediaId } // remove unneeded info from the media title before searching diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index 449919d..909e766 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -44,6 +44,78 @@ data class ItemMedia( /** * TODO the episodes workflow could use a clean up/rework */ +// TODO replace playlist: List with a map? +data class AoDMedia( + val aodId: Int, + val type: DataTypes.MediaType, + val title: String, + val shortText: String, + val posterURL: String, + var year: Int, + var age: Int, + val similar: List, + val playlist: List, +) { + fun hasEpisode(mediaId: Int) = playlist.any { it.mediaId == mediaId } + fun getEpisodeById(mediaId: Int) = playlist.firstOrNull { it.mediaId == mediaId } + ?: AoDEpisodeNone +} + +data class AoDEpisode( + val mediaId: Int, + val title: String, + val description: String, + val shortDesc: String, + val imageURL: String, + val number: Int, + var watched: Boolean, + val watchedCallback: String, + val streams: MutableList, +){ + fun hasDub() = streams.any { it.language == Locale.GERMAN } + + /** + * get the preferred stream + * @return the preferred stream, if not present use the first stream + */ + fun getPreferredStream(language: Locale) = streams.firstOrNull { it.language == language } + ?: streams.first() +} + +// TODO will be watched info (state and callback) -> remove description and number +data class AoDEpisodeInfo( + val aodMediaId: Int, + val shortDesc: String, + var watched: Boolean, + val watchedCallback: String, +) + +val AoDMediaNone = AoDMedia( + -1, + DataTypes.MediaType.OTHER, + "", + "", + "", + -1, + -1, + listOf(), + listOf() +) + +val AoDEpisodeNone = AoDEpisode( + -1, + "", + "", + "", + "", + -1, + false, + "", + mutableListOf() +) + +// LEGACY + data class Media( val id: Int, val link: String, 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 89b0bf5..62416d9 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 @@ -11,10 +11,10 @@ import com.bumptech.glide.request.RequestOptions 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.AoDEpisode import org.mosad.teapod.util.tmdb.TMDBTVEpisode -class EpisodeItemAdapter(private val episodes: List, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { +class EpisodeItemAdapter(private val episodes: List, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { var onImageClick: ((String, Int) -> Unit)? = null @@ -41,8 +41,8 @@ class EpisodeItemAdapter(private val episodes: List, private val tmdbEp "" } - if (episodes[position].posterUrl.isNotEmpty()) { - Glide.with(context).load(ep.posterUrl) + if (ep.imageURL.isNotEmpty()) { + Glide.with(context).load(ep.imageURL) .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0))) .into(holder.binding.imageEpisode) diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt index 81eee9c..43da35b 100644 --- a/fastlane/metadata/android/de/full_description.txt +++ b/fastlane/metadata/android/de/full_description.txt @@ -1,6 +1,6 @@ Teapod ist eine inoffizielle App für Anime-on-Demand (AoD). -* Schau dir alle Title von AoD auf deinem Android Gerät an +* Schau dir alle Titel von AoD auf deinem Android Gerät an * Nativer Player auf Basis des ExoPayers * Bevorzuge die OmU Version über die App-Einstellungen * Speicher deine lieblings Anime in "Meine Liste" -- 2.44.0 From ed9eff433b918b23a2f92d777686eca9551830aa Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 4 Sep 2021 13:33:46 +0200 Subject: [PATCH 10/14] AoDParser Media handling rework [Part 2/2] * move Player to new AoD media Implementation * remove old AoD media Implementation from AoDParser --- .../java/org/mosad/teapod/parser/AoDParser.kt | 188 ++---------------- .../activity/main/fragments/HomeFragment.kt | 2 +- .../activity/main/fragments/MediaFragment.kt | 51 +++-- .../main/fragments/MediaFragmentEpisodes.kt | 8 +- .../main/fragments/MediaFragmentSimilar.kt | 2 +- .../main/viewmodel/MediaFragmentViewModel.kt | 43 ++-- .../ui/activity/player/PlayerActivity.kt | 2 +- .../ui/activity/player/PlayerViewModel.kt | 25 +-- .../ui/components/EpisodesListPlayer.kt | 4 +- .../java/org/mosad/teapod/util/DataTypes.kt | 61 +----- .../util/adapter/PlayerEpisodeItemAdapter.kt | 8 +- 11 files changed, 81 insertions(+), 313 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 95ea3a4..11627ba 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -31,7 +31,6 @@ import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.util.* import org.mosad.teapod.util.DataTypes.MediaType import java.io.IOException -import java.lang.NumberFormatException import java.util.* import kotlin.random.Random @@ -48,7 +47,6 @@ object AoDParser { private var csrfToken: String = "" private var loginSuccess = false - private val mediaList = arrayListOf() // actual media (data) TODO remove private val aodMediaList = arrayListOf() // actual media (data) // gui media @@ -109,29 +107,12 @@ object AoDParser { } } - /** - * get a media by it's ID (int) - * @return Media - */ - @Deprecated(message = "Use getMediaById2() instead") - suspend fun getMediaById(aodId: Int): Media { - val media = mediaList.first { it.id == aodId } - - if (media.episodes.isEmpty()) { - loadStreams(media).join() - - loadMediaAsync(media.id).await() - } - - return media - } - /** * get a media by it's ID (int) * @param aodId The AoD ID of the requested media * @return returns a AoDMedia of type Movie or TVShow if found, else return AoDMediaNone */ - suspend fun getMediaById2(aodId: Int): AoDMedia { + suspend fun getMediaById(aodId: Int): AoDMedia { return aodMediaList.firstOrNull { it.aodId == aodId } ?: try { loadMediaAsync(aodId).await().apply { @@ -141,8 +122,6 @@ object AoDParser { Log.e(javaClass.name, "Error while loading media $aodId", exn) AoDMediaNone } - - } /** @@ -165,12 +144,12 @@ object AoDParser { return baseUrl + subscriptionPath } - suspend fun markAsWatched(mediaId: Int, episodeId: Int) { - val episode = getMediaById(mediaId).getEpisodeById(episodeId) + suspend fun markAsWatched(aodId: Int, episodeId: Int) { + val episode = getMediaById(aodId).getEpisodeById(episodeId) episode.watched = true sendCallback(episode.watchedCallback) - Log.d(javaClass.name, "Marked episode ${episode.id} as watched") + Log.d(javaClass.name, "Marked episode ${episode.mediaId} as watched") } // TODO don't use jsoup here @@ -206,7 +185,6 @@ object AoDParser { //println(resAnimes) guiMediaList.clear() - mediaList.clear() val animes = resAnimes.select("div.animebox") guiMediaList.addAll( @@ -221,28 +199,7 @@ object AoDParser { } ) - // TODO legacy - resAnimes.select("div.animebox").forEach { - val id = it.select("p.animebox-link").select("a").attr("href") - .substringAfterLast("/").toInt() - val title = it.select("h3.animebox-title").text() - val image = it.select("p.animebox-image").select("img").attr("src") - val link = it.select("p.animebox-link").select("a").attr("href") - val type = when (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT)) { - "zur serie" -> MediaType.TVSHOW - "zum film" -> MediaType.MOVIE - else -> MediaType.OTHER - } - val mediaShortText = it.select("p.animebox-shorttext").text() - - mediaList.add(Media(id, link, type).apply { - info.title = title - info.posterUrl = image - info.shortDesc = mediaShortText - }) - } - - Log.i(javaClass.name, "Total library size is: ${mediaList.size}") + Log.i(javaClass.name, "Total library size is: ${guiMediaList.size}") } } @@ -332,121 +289,17 @@ object AoDParser { } /** - * TODO rework the media loading process, don't modify media object * TODO catch SocketTimeoutException from loading to show a waring dialog - * load streams for the media path, movies have one episode - * @param media is used as call ba reference + * Load media async. Every media has a playlist. + * @param aodId The AoD ID of the requested media */ - private suspend fun loadStreams(media: Media) = coroutineScope { - launch(Dispatchers.IO) { - if (sessionCookies.isEmpty()) login() - - if (!loginSuccess) { - Log.w(javaClass.name, "Login, was not successful.") - return@launch - } - - // get the media page - val res = Jsoup.connect(baseUrl + media.link) - .cookies(sessionCookies) - .get() - - //println(res) - - if (csrfToken.isEmpty()) { - csrfToken = res.select("meta[name=csrf-token]").attr("content") - //Log.i(javaClass.name, "New csrf token is $csrfToken") - } - - val besides = res.select("div.besides").first() - val playlists = besides.select("input.streamstarter_html5").map { streamstarter -> - parsePlaylistAsync( - streamstarter.attr("data-playlist"), - streamstarter.attr("data-lang") - ) - }.awaitAll() - - playlists.forEach { aod -> - aod.list.forEach { ep -> - try { - if (media.hasEpisode(ep.mediaid)) { - media.getEpisodeById(ep.mediaid).streams.add( - Stream(ep.sources.first().file, aod.language) - ) - } else { - media.episodes.add(Episode( - id = ep.mediaid, - streams = mutableListOf(Stream(ep.sources.first().file, aod.language)), - posterUrl = ep.image, - title = ep.title, - description = ep.description, - number = getNumberFromTitle(ep.title, media.type) - )) - } - } catch (ex: Exception) { - Log.w(javaClass.name, "Could not parse episode information.", ex) - } - } - } - Log.i(javaClass.name, "Loaded playlists successfully") - - // additional info from the media page - res.select("table.vertical-table").select("tr").forEach { row -> - when (row.select("th").text().lowercase(Locale.ROOT)) { - "produktionsjahr" -> media.info.year = row.select("td").text().toInt() - "fsk" -> media.info.age = row.select("td").text().toInt() - "episodenanzahl" -> { - media.info.episodesCount = row.select("td").text() - .substringBefore("/") - .filter { it.isDigit() } - .toInt() - } - } - } - - // similar titles from media page - media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() - - if (mediaId != null) { - ItemMedia(mediaId, mediaTitle, mediaImage) - } else { - null - } - } - - // additional information for tv shows the episode title (description) is loaded from the "api" - if (media.type == MediaType.TVSHOW) { - res.select("div.three-box-container > div.episodebox").forEach { episodebox -> - // make sure the episode has a streaming link - if (episodebox.select("input.streamstarter_html5").isNotEmpty()) { - val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() - val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() - val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") - val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() - - media.episodes.firstOrNull { it.id == episodeId }?.apply { - shortDesc = episodeShortDesc - watched = episodeWatched - watchedCallback = episodeWatchedCallback - } - } - } - } - Log.i(javaClass.name, "media loaded successfully") - } - } - private suspend fun loadMediaAsync(aodId: Int): Deferred = coroutineScope { return@coroutineScope async (Dispatchers.IO) { if (sessionCookies.isEmpty()) login() // TODO is this needed? // return none object, if login wasn't successful if (!loginSuccess) { - Log.w(javaClass.name, "Login, was not successful.") + Log.w(javaClass.name, "Login was not successful") return@async AoDMediaNone } @@ -461,7 +314,7 @@ object AoDParser { Log.d(javaClass.name, "New csrf token is $csrfToken") } - // playlist parsing TODO can this be async to the genral info marsing? + // playlist parsing TODO can this be async to the general info parsing? val besides = res.select("div.besides").first() val aodPlaylists = besides.select("input.streamstarter_html5").map { streamstarter -> parsePlaylistAsync( @@ -501,6 +354,7 @@ object AoDParser { if (mediaId != null) { ItemMedia(mediaId, mediaTitle, mediaImage) } else { + Log.i(javaClass.name, "MediaId for similar to $aodId was null") null } } @@ -522,6 +376,7 @@ object AoDParser { AoDEpisodeInfo(mediaId, episodeShortDesc, episodeWatched, episodeWatchedCallback) } else { + Log.i(javaClass.name, "Episode info for $aodId has empty streamstarter_html5 ") null } }.associateBy { it.aodMediaId } @@ -529,7 +384,7 @@ object AoDParser { mapOf() } - // TODO make AoDPlaylist to teapod playlist + // map the aod api playlist to a teapod playlist val playlist: List = aodPlaylists.awaitAll().flatMap { aodPlaylist -> aodPlaylist.list.mapIndexed { index, episode -> AoDEpisode( @@ -549,7 +404,6 @@ object AoDParser { it.streams.addAll(element.streams) } }.values.toList() - println("new playlist object: $playlist") return@async AoDMedia( aodId = aodId, @@ -615,22 +469,4 @@ object AoDParser { } } - /** - * get the episode number from the title - * @param title the episode title, containing a number after "Ep." - * @param type the media type, if not TVSHOW, return 0 - * @return the episode number, on NumberFormatException return 0 - */ - private fun getNumberFromTitle(title: String, type: MediaType): Int { - return if (type == MediaType.TVSHOW) { - try { - title.substringAfter(", Ep. ").toInt() - } catch (nex: NumberFormatException) { - 0 - } - } else { - 0 - } - } - } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index e9422f1..ae7ddd4 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -96,7 +96,7 @@ class HomeFragment : Fragment() { binding.buttonPlayHighlight.setOnClickListener { // TODO get next episode lifecycleScope.launch { - val media = AoDParser.getMediaById2(highlightMedia.id) + val media = AoDParser.getMediaById(highlightMedia.id) Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}") (activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId) 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 2d8cef3..082a100 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 @@ -56,7 +56,7 @@ class MediaFragment(private val mediaId: Int) : Fragment() { binding.pagerEpisodesSimilar.offscreenPageLimit = 2 binding.pagerEpisodesSimilar.adapter = pagerAdapter TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position -> - tab.text = if (model.media2.type == MediaType.TVSHOW && position == 0) { + tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) { getString(R.string.episodes) } else { getString(R.string.similar_titles) @@ -75,9 +75,8 @@ class MediaFragment(private val mediaId: Int) : Fragment() { super.onResume() // update the next ep text if there is one, since it may have changed - println(model.nextEpisodeId) - if (model.media2.getEpisodeById(model.nextEpisodeId).title.isNotEmpty()) { - binding.textTitle.text = model.media2.getEpisodeById(model.nextEpisodeId).title + if (model.media.getEpisodeById(model.nextEpisodeId).title.isNotEmpty()) { + binding.textTitle.text = model.media.getEpisodeById(model.nextEpisodeId).title } } @@ -87,9 +86,9 @@ class MediaFragment(private val mediaId: Int) : Fragment() { private fun updateGUI() = with(model) { // generic gui val backdropUrl = tmdbResult?.backdropPath?.let { TMDBApiController.imageUrl + it } - ?: media2.posterURL + ?: media.posterURL val posterUrl = tmdbResult?.posterPath?.let { TMDBApiController.imageUrl + it } - ?: media2.posterURL + ?: media.posterURL // load poster and backdrop Glide.with(requireContext()).load(posterUrl) @@ -99,13 +98,13 @@ class MediaFragment(private val mediaId: Int) : Fragment() { .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) .into(binding.imageBackdrop) - binding.textTitle.text = media2.title - binding.textYear.text = media2.year.toString() - binding.textAge.text = media2.age.toString() - binding.textOverview.text = media2.shortText + binding.textTitle.text = media.title + binding.textYear.text = media.year.toString() + binding.textAge.text = media.age.toString() + binding.textOverview.text = media.shortText // set "my list" indicator - if (StorageController.myList.contains(media2.aodId)) { + if (StorageController.myList.contains(media.aodId)) { Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) } else { Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) @@ -116,25 +115,25 @@ class MediaFragment(private val mediaId: Int) : Fragment() { pagerAdapter.notifyDataSetChanged() // specific gui - if (media2.type == MediaType.TVSHOW) { + if (media.type == MediaType.TVSHOW) { // get next episode - nextEpisodeId = media2.playlist.firstOrNull{ !it.watched }?.mediaId - ?: media2.playlist.first().mediaId + nextEpisodeId = media.playlist.firstOrNull{ !it.watched }?.mediaId + ?: media.playlist.first().mediaId // title is the next episodes title - binding.textTitle.text = media2.getEpisodeById(nextEpisodeId).title + binding.textTitle.text = media.getEpisodeById(nextEpisodeId).title // episodes count binding.textEpisodesOrRuntime.text = resources.getQuantityString( R.plurals.text_episodes_count, - media2.playlist.size, - media2.playlist.size + media.playlist.size, + media.playlist.size ) // episodes fragments.add(MediaFragmentEpisodes()) pagerAdapter.notifyDataSetChanged() - } else if (media2.type == MediaType.MOVIE) { + } else if (media.type == MediaType.MOVIE) { val tmdbMovie = (tmdbResult as TMDBMovie?) if (tmdbMovie?.runtime != null) { @@ -149,7 +148,7 @@ class MediaFragment(private val mediaId: Int) : Fragment() { } // if has similar titles - if (media2.similar.isNotEmpty()) { + if (media.similar.isNotEmpty()) { fragments.add(MediaFragmentSimilar()) pagerAdapter.notifyDataSetChanged() } @@ -165,20 +164,20 @@ class MediaFragment(private val mediaId: Int) : Fragment() { private fun initActions() = with(model) { binding.buttonPlay.setOnClickListener { - when (media2.type) { - MediaType.MOVIE -> playEpisode(media2.playlist.first().mediaId) + when (media.type) { + MediaType.MOVIE -> playEpisode(media.playlist.first().mediaId) MediaType.TVSHOW -> playEpisode(nextEpisodeId) - else -> Log.e(javaClass.name, "Wrong Type: ${media2.type}") + else -> Log.e(javaClass.name, "Wrong Type: ${media.type}") } } // add or remove media from myList binding.linearMyListAction.setOnClickListener { - if (StorageController.myList.contains(media2.aodId)) { - StorageController.myList.remove(media2.aodId) + if (StorageController.myList.contains(media.aodId)) { + StorageController.myList.remove(media.aodId) Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) } else { - StorageController.myList.add(media2.aodId) + StorageController.myList.add(media.aodId) Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) } StorageController.saveMyList(requireContext()) @@ -195,7 +194,7 @@ class MediaFragment(private val mediaId: Int) : Fragment() { * TODO this is also used in MediaFragmentEpisode, we should only have on implementation */ private fun playEpisode(episodeId: Int) { - (activity as MainActivity).startPlayer(model.media2.aodId, episodeId) + (activity as MainActivity).startPlayer(model.media.aodId, episodeId) Log.d(javaClass.name, "Started Player with episodeId: $episodeId") model.updateNextEpisode(episodeId) // set the correct next episode 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 7a0eff9..f2e9f58 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 @@ -27,13 +27,13 @@ class MediaFragmentEpisodes : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - adapterRecEpisodes = EpisodeItemAdapter(model.media2.playlist, model.tmdbTVSeason?.episodes) + adapterRecEpisodes = EpisodeItemAdapter(model.media.playlist, model.tmdbTVSeason?.episodes) binding.recyclerEpisodes.adapter = adapterRecEpisodes // set onItemClick only in adapter is initialized if (this::adapterRecEpisodes.isInitialized) { adapterRecEpisodes.onImageClick = { _, position -> - playEpisode(model.media2.playlist[position].mediaId) + playEpisode(model.media.playlist[position].mediaId) } } } @@ -43,7 +43,7 @@ class MediaFragmentEpisodes : Fragment() { // if adapterRecEpisodes is initialized, update the watched state for the episodes if (this::adapterRecEpisodes.isInitialized) { - model.media2.playlist.forEachIndexed { index, episodeInfo -> + model.media.playlist.forEachIndexed { index, episodeInfo -> adapterRecEpisodes.updateWatchedState(episodeInfo.watched, index) } adapterRecEpisodes.notifyDataSetChanged() @@ -51,7 +51,7 @@ class MediaFragmentEpisodes : Fragment() { } private fun playEpisode(episodeId: Int) { - (activity as MainActivity).startPlayer(model.media2.aodId, episodeId) + (activity as MainActivity).startPlayer(model.media.aodId, episodeId) Log.d(javaClass.name, "Started Player with episodeId: $episodeId") model.updateNextEpisode(episodeId) // set the correct next episode diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt index dba70c3..87195a1 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragmentSimilar.kt @@ -27,7 +27,7 @@ class MediaFragmentSimilar : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - adapterSimilar = MediaItemAdapter(model.media2.similar) + adapterSimilar = MediaItemAdapter(model.media.similar) binding.recyclerMediaSimilar.adapter = adapterSimilar binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9)) 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 fd1f4b6..95d9887 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 @@ -16,17 +16,11 @@ import org.mosad.teapod.util.tmdb.TMDBTVSeason */ class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { -// var media = Media(-1, "", MediaType.OTHER) -// internal set -// var nextEpisode = Episode() -// internal set - - var media2 = AoDMediaNone + var media = AoDMediaNone internal set var nextEpisodeId = -1 internal set - var tmdbResult: TMDBResult? = null // TODO rename internal set var tmdbTVSeason: TMDBTVSeason? =null @@ -38,18 +32,17 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic * set media, tmdb and nextEpisode * TODO run aod and tmdb load parallel */ - suspend fun load(mediaId: Int) { + suspend fun load(aodId: Int) { val tmdbApiController = TMDBApiController() - //media = AoDParser.getMediaById(mediaId) - media2 = AoDParser.getMediaById2(mediaId) + media = AoDParser.getMediaById(aodId) // check if metaDB knows the title - val tmdbId: Int = if (MetaDBController.mediaList.media.contains(media2.aodId)) { + val tmdbId: Int = if (MetaDBController.mediaList.media.contains(aodId)) { // load media info from metaDB val metaDB = MetaDBController() - mediaMeta = when (media2.type) { - MediaType.MOVIE -> metaDB.getMovieMetadata(media2.aodId) - MediaType.TVSHOW -> metaDB.getTVShowMetadata(media2.aodId) + mediaMeta = when (media.type) { + MediaType.MOVIE -> metaDB.getMovieMetadata(media.aodId) + MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.aodId) else -> null } @@ -57,28 +50,27 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic } else { // use tmdb search to get media info mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media - tmdbApiController.search(stripTitleInfo(media2.title), media2.type) + tmdbApiController.search(stripTitleInfo(media.title), media.type) } - tmdbResult = when (media2.type) { + tmdbResult = when (media.type) { MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) else -> null } - println(tmdbResult) // TODO // get season info, if metaDB knows the tv show - tmdbTVSeason = if (media2.type == MediaType.TVSHOW && mediaMeta != null) { + tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) { val tvShowMeta = mediaMeta as TVShowMeta tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) } else { null } - if (media2.type == MediaType.TVSHOW) { + if (media.type == MediaType.TVSHOW) { //nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() - nextEpisodeId = media2.playlist.firstOrNull { !it.watched }?.mediaId - ?: media2.playlist.first().mediaId + nextEpisodeId = media.playlist.firstOrNull { !it.watched }?.mediaId + ?: media.playlist.first().mediaId } } @@ -87,13 +79,10 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic * if no matching is found, use first episode */ fun updateNextEpisode(episodeId: Int) { - if (media2.type == MediaType.MOVIE) return // return if movie + if (media.type == MediaType.MOVIE) return // return if movie -// nextEpisode = media.episodes.firstOrNull{ it.number > currentEp.number } -// ?: media.episodes.first() - - nextEpisodeId = media2.playlist.firstOrNull { it.number > media2.getEpisodeById(episodeId).number }?.mediaId - ?: media2.playlist.first().mediaId + nextEpisodeId = media.playlist.firstOrNull { it.number > media.getEpisodeById(episodeId).number }?.mediaId + ?: media.playlist.first().mediaId } // remove unneeded info from the media title before searching diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index 24682f5..a1307d8 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -171,7 +171,7 @@ class PlayerActivity : AppCompatActivity() { } private fun initPlayer() { - if (model.media.id < 0) { + if (model.media.aodId < 0) { Log.e(javaClass.name, "No media was set.") this.finish() } 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 2894e21..9dffdd1 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 @@ -40,11 +40,11 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) val currentEpisodeChangedListener = ArrayList<() -> Unit>() private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN - var media: Media = Media(-1, "", DataTypes.MediaType.OTHER) + var media: AoDMedia = AoDMediaNone internal set - var currentEpisode = Episode() + var currentEpisode = AoDEpisodeNone internal set - var nextEpisode: Episode? = null + var nextEpisode: AoDEpisode? = null internal set var mediaMeta: Meta? = null internal set @@ -80,12 +80,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) fun loadMedia(mediaId: Int, episodeId: Int) { runBlocking { media = AoDParser.getMediaById(mediaId) - mediaMeta = loadMediaMeta(media.id) // can be done blocking, since it should be cached + mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached } currentEpisode = media.getEpisodeById(episodeId) nextEpisode = selectNextEpisode() - currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.id) + currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language } @@ -122,12 +122,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) * * updateWatchedState for the next (now current) episode */ - fun playEpisode(episode: Episode, replace: Boolean = false, seekPosition: Long = 0) { + fun playEpisode(episode: AoDEpisode, replace: Boolean = false, seekPosition: Long = 0) { val preferredStream = episode.getPreferredStream(currentLanguage) currentLanguage = preferredStream.language // update current language, since it may have changed currentEpisode = episode nextEpisode = selectNextEpisode() - currentEpisodeMeta = getEpisodeMetaByAoDMediaId(episode.id) + currentEpisodeMeta = getEpisodeMetaByAoDMediaId(episode.mediaId) currentEpisodeChangedListener.forEach { it() } // update player gui (title) val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( @@ -138,7 +138,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) // if episodes has not been watched, mark as watched if (!episode.watched) { viewModelScope.launch { - AoDParser.markAsWatched(media.id, episode.id) + AoDParser.markAsWatched(media.aodId, episode.mediaId) } } } @@ -188,13 +188,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) * Based on the current episodeId, get the next episode. If there is no next * episode, return null */ - private fun selectNextEpisode(): Episode? { - val nextEpIndex = media.episodes.indexOfFirst { it.id == currentEpisode.id } + 1 - return if (nextEpIndex < media.episodes.size) { - media.episodes[nextEpIndex] - } else { - null - } + private fun selectNextEpisode(): AoDEpisode? { + return media.playlist.firstOrNull { it.number > media.getEpisodeById(currentEpisode.mediaId).number } } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt index cb51deb..f7fe649 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt @@ -28,11 +28,11 @@ class EpisodesListPlayer @JvmOverloads constructor( } model?.let { - adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.episodes) + adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.playlist) adapterRecEpisodes.onImageClick = { _, position -> (this.parent as ViewGroup).removeView(this) - model.playEpisode(model.media.episodes[position], replace = true) + model.playEpisode(model.media.playlist[position], replace = true) } adapterRecEpisodes.currentSelected = model.currentEpisode.number - 1 diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index 909e766..dea2d20 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -82,6 +82,11 @@ data class AoDEpisode( ?: streams.first() } +data class Stream( + val url: String, + val language : Locale +) + // TODO will be watched info (state and callback) -> remove description and number data class AoDEpisodeInfo( val aodMediaId: Int, @@ -114,62 +119,6 @@ val AoDEpisodeNone = AoDEpisode( mutableListOf() ) -// LEGACY - -data class Media( - val id: Int, - val link: String, - val type: DataTypes.MediaType, - val info: Info = Info(), - val episodes: ArrayList = arrayListOf() -) { - fun hasEpisode(id: Int) = episodes.any { it.id == id } - fun getEpisodeById(id: Int) = episodes.first { it.id == id } -} - -/** - * uses var, since the values are written in different steps - */ -data class Info( - var title: String = "", - var posterUrl: String = "", - var shortDesc: String = "", - var description: String = "", - var year: Int = 0, - var age: Int = 0, - var episodesCount: Int = 0, - var similar: List = listOf() -) - -/** - * number = episode number (0..n) - */ -data class Episode( - val id: Int = -1, - val streams: MutableList = mutableListOf(), - val title: String = "", - val posterUrl: String = "", - val description: String = "", - var shortDesc: String = "", - val number: Int = 0, - var watched: Boolean = false, - var watchedCallback: String = "" -) { - /** - * get the preferred stream - * @return the preferred stream, if not present use the first stream - */ - fun getPreferredStream(language: Locale) = - streams.firstOrNull { it.language == language } ?: streams.first() - - fun hasDub() = streams.any { it.language == Locale.GERMAN } -} - -data class Stream( - val url: String, - val language : Locale -) - /** * this class is used to represent the aod json API? */ diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt index 8b005a7..e1d37d5 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt @@ -9,9 +9,9 @@ import com.bumptech.glide.request.RequestOptions import jp.wasabeef.glide.transformations.RoundedCornersTransformation import org.mosad.teapod.R import org.mosad.teapod.databinding.ItemEpisodePlayerBinding -import org.mosad.teapod.util.Episode +import org.mosad.teapod.util.AoDEpisode -class PlayerEpisodeItemAdapter(private val episodes: List) : RecyclerView.Adapter() { +class PlayerEpisodeItemAdapter(private val episodes: List) : RecyclerView.Adapter() { var onImageClick: ((String, Int) -> Unit)? = null var currentSelected: Int = -1 // -1, since position should never be < 0 @@ -33,8 +33,8 @@ class PlayerEpisodeItemAdapter(private val episodes: List) : RecyclerVi holder.binding.textEpisodeTitle2.text = titleText holder.binding.textEpisodeDesc2.text = ep.shortDesc - if (episodes[position].posterUrl.isNotEmpty()) { - Glide.with(context).load(ep.posterUrl) + if (ep.imageURL.isNotEmpty()) { + Glide.with(context).load(ep.imageURL) .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0))) .into(holder.binding.imageEpisode) } -- 2.44.0 From 062013489d0c8781a17ca51d26940207eef1ab21 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 5 Sep 2021 00:04:59 +0200 Subject: [PATCH 11/14] use notifyItem...() instead of notifyDataSetChanged() in MediaFragment --- .../ui/activity/main/fragments/MediaFragment.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 082a100..fb2cc42 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 @@ -111,8 +111,9 @@ class MediaFragment(private val mediaId: Int) : Fragment() { } // clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction) + val fragmentsSize = if (fragments.lastIndex < 0) 0 else fragments.lastIndex fragments.clear() - pagerAdapter.notifyDataSetChanged() + pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize) // specific gui if (media.type == MediaType.TVSHOW) { @@ -131,8 +132,10 @@ class MediaFragment(private val mediaId: Int) : Fragment() { ) // episodes - fragments.add(MediaFragmentEpisodes()) - pagerAdapter.notifyDataSetChanged() + MediaFragmentEpisodes().also { + fragments.add(it) + pagerAdapter.notifyItemInserted(fragments.indexOf(it)) + } } else if (media.type == MediaType.MOVIE) { val tmdbMovie = (tmdbResult as TMDBMovie?) @@ -149,8 +152,10 @@ class MediaFragment(private val mediaId: Int) : Fragment() { // if has similar titles if (media.similar.isNotEmpty()) { - fragments.add(MediaFragmentSimilar()) - pagerAdapter.notifyDataSetChanged() + MediaFragmentSimilar().also { + fragments.add(it) + pagerAdapter.notifyItemInserted(fragments.indexOf(it)) + } } // disable scrolling on appbar, if no tabs where added -- 2.44.0 From 5ea94b7ded9e77a10207326aefe0a704418799af Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 5 Sep 2021 00:08:03 +0200 Subject: [PATCH 12/14] add numberStr to AoDEpisode type & show tmdb episode info in player * use numberStr instead of index to display the correct episode number, allowing for number such as "12.5" * show tmdb episode description in player if found and aod description is missing --- .../java/org/mosad/teapod/parser/AoDParser.kt | 3 ++- .../main/viewmodel/MediaFragmentViewModel.kt | 2 +- .../ui/activity/player/PlayerViewModel.kt | 17 +++++++++++++++-- .../teapod/ui/components/EpisodesListPlayer.kt | 7 +++---- .../java/org/mosad/teapod/util/DataTypes.kt | 11 ++++------- .../teapod/util/adapter/EpisodeItemAdapter.kt | 4 ++-- .../util/adapter/PlayerEpisodeItemAdapter.kt | 15 +++++++++++---- app/src/main/res/layout/item_episode_player.xml | 3 ++- app/src/main/res/values-de-rDE/strings.xml | 4 ++-- app/src/main/res/values/strings.xml | 4 ++-- 10 files changed, 44 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 11627ba..03fa36a 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -393,7 +393,8 @@ object AoDParser { description = episode.description, shortDesc = episodesInfo[episode.mediaid]?.shortDesc ?: "", imageURL = episode.image, - number = index, + numberStr = episode.title.substringAfter(", Ep. ", ""), // TODO move to parsePalylist + index = index, watched = episodesInfo[episode.mediaid]?.watched ?: false, watchedCallback = episodesInfo[episode.mediaid]?.watchedCallback ?: "", streams = mutableListOf(Stream(episode.sources.first().file, aodPlaylist.language)) 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 95d9887..6f855d9 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 @@ -81,7 +81,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic fun updateNextEpisode(episodeId: Int) { if (media.type == MediaType.MOVIE) return // return if movie - nextEpisodeId = media.playlist.firstOrNull { it.number > media.getEpisodeById(episodeId).number }?.mediaId + nextEpisodeId = media.playlist.firstOrNull { it.index > media.getEpisodeById(episodeId).index }?.mediaId ?: media.playlist.first().mediaId } 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 9dffdd1..e63225a 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 @@ -20,6 +20,8 @@ import org.mosad.teapod.R import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.util.* +import org.mosad.teapod.util.tmdb.TMDBApiController +import org.mosad.teapod.util.tmdb.TMDBTVSeason import java.util.* import kotlin.collections.ArrayList @@ -46,6 +48,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) internal set var nextEpisode: AoDEpisode? = null internal set + var tmdbTVSeason: TMDBTVSeason? =null + internal set var mediaMeta: Meta? = null internal set var currentEpisodeMeta: EpisodeMeta? = null @@ -83,6 +87,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached } + // run async as it should be loaded by the time the episodes a + viewModelScope.launch { + // get season info, if metaDB knows the tv show + if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) { + val tvShowMeta = mediaMeta as TVShowMeta + //tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) + } + } + currentEpisode = media.getEpisodeById(episodeId) nextEpisode = selectNextEpisode() currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) @@ -159,7 +172,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) return if (media.type == DataTypes.MediaType.TVSHOW) { getApplication().getString( R.string.component_episode_title, - currentEpisode.number, + currentEpisode.numberStr, currentEpisode.description ) } else { @@ -189,7 +202,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) * episode, return null */ private fun selectNextEpisode(): AoDEpisode? { - return media.playlist.firstOrNull { it.number > media.getEpisodeById(currentEpisode.mediaId).number } + return media.playlist.firstOrNull { it.index > media.getEpisodeById(currentEpisode.mediaId).index } } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt index f7fe649..9a7874d 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt @@ -28,16 +28,15 @@ class EpisodesListPlayer @JvmOverloads constructor( } model?.let { - adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.playlist) - + adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.playlist, model.tmdbTVSeason?.episodes) adapterRecEpisodes.onImageClick = { _, position -> (this.parent as ViewGroup).removeView(this) model.playEpisode(model.media.playlist[position], replace = true) } - adapterRecEpisodes.currentSelected = model.currentEpisode.number - 1 + adapterRecEpisodes.currentSelected = model.currentEpisode.index binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes - binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.number - 1) // number != index + binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.index) } } diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index dea2d20..db662e5 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -1,7 +1,6 @@ package org.mosad.teapod.util -import java.util.* -import kotlin.collections.ArrayList +import java.util.Locale class DataTypes { enum class MediaType { @@ -41,9 +40,6 @@ data class ItemMedia( val posterUrl: String ) -/** - * TODO the episodes workflow could use a clean up/rework - */ // TODO replace playlist: List with a map? data class AoDMedia( val aodId: Int, @@ -56,7 +52,6 @@ data class AoDMedia( val similar: List, val playlist: List, ) { - fun hasEpisode(mediaId: Int) = playlist.any { it.mediaId == mediaId } fun getEpisodeById(mediaId: Int) = playlist.firstOrNull { it.mediaId == mediaId } ?: AoDEpisodeNone } @@ -67,7 +62,8 @@ data class AoDEpisode( val description: String, val shortDesc: String, val imageURL: String, - val number: Int, + val numberStr: String, + val index: Int, var watched: Boolean, val watchedCallback: String, val streams: MutableList, @@ -113,6 +109,7 @@ val AoDEpisodeNone = AoDEpisode( "", "", "", + "", -1, false, "", 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 62416d9..3bd2df0 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 @@ -27,9 +27,9 @@ class EpisodeItemAdapter(private val episodes: List, private val tmd val ep = episodes[position] val titleText = if (ep.hasDub()) { - context.getString(R.string.component_episode_title, ep.number, ep.description) + context.getString(R.string.component_episode_title, ep.numberStr, ep.description) } else { - context.getString(R.string.component_episode_title_sub, ep.number, ep.description) + context.getString(R.string.component_episode_title_sub, ep.numberStr, ep.description) } holder.binding.textEpisodeTitle.text = titleText diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt index e1d37d5..6cf35a0 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt @@ -10,8 +10,9 @@ import jp.wasabeef.glide.transformations.RoundedCornersTransformation import org.mosad.teapod.R import org.mosad.teapod.databinding.ItemEpisodePlayerBinding import org.mosad.teapod.util.AoDEpisode +import org.mosad.teapod.util.tmdb.TMDBTVEpisode -class PlayerEpisodeItemAdapter(private val episodes: List) : RecyclerView.Adapter() { +class PlayerEpisodeItemAdapter(private val episodes: List, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { var onImageClick: ((String, Int) -> Unit)? = null var currentSelected: Int = -1 // -1, since position should never be < 0 @@ -25,13 +26,19 @@ class PlayerEpisodeItemAdapter(private val episodes: List) : Recycle val ep = episodes[position] val titleText = if (ep.hasDub()) { - context.getString(R.string.component_episode_title, ep.number, ep.description) + context.getString(R.string.component_episode_title, ep.numberStr, ep.description) } else { - context.getString(R.string.component_episode_title_sub, ep.number, ep.description) + context.getString(R.string.component_episode_title_sub, ep.numberStr, ep.description) } holder.binding.textEpisodeTitle2.text = titleText - holder.binding.textEpisodeDesc2.text = ep.shortDesc + holder.binding.textEpisodeDesc2.text = if (ep.shortDesc.isNotEmpty()) { + ep.shortDesc + } else if (tmdbEpisodes != null && position < tmdbEpisodes.size){ + tmdbEpisodes[position].overview + } else { + "" + } if (ep.imageURL.isNotEmpty()) { Glide.with(context).load(ep.imageURL) diff --git a/app/src/main/res/layout/item_episode_player.xml b/app/src/main/res/layout/item_episode_player.xml index 4be2bc5..4b97df5 100644 --- a/app/src/main/res/layout/item_episode_player.xml +++ b/app/src/main/res/layout/item_episode_player.xml @@ -51,7 +51,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="5dp" + android:maxLines="10" android:text="@string/text_overview_ex" - android:textColor="@color/textPrimaryDark"/> + android:textColor="@color/textPrimaryDark" /> \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index a957051..d7ed368 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -27,8 +27,8 @@ %d Minuten Ähnliche Titel - Flg. %1$d %2$s - Flg. %1$d %2$s (OmU) + Flg. %1$s %2$s + Flg. %1$s %2$s (OmU) Account diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a58f3a2..ce053ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,8 +34,8 @@ %d Minutes Similar titles - Ep. %1$d %2$s - Ep. %1$d %2$s (Sub) + Ep. %1$s %2$s + Ep. %1$s %2$s (Sub) episode poster already watched -- 2.44.0 From 8753d4f36f69edbf4d8e8b219c252881a231e903 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 5 Sep 2021 00:08:53 +0200 Subject: [PATCH 13/14] fix tmdb episode description in player --- .../java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e63225a..194110d 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 @@ -92,7 +92,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) // get season info, if metaDB knows the tv show if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) { val tvShowMeta = mediaMeta as TVShowMeta - //tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) + tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) } } -- 2.44.0 From eeb1c33e432868e8ebcae47475b21649908f86fa Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 5 Sep 2021 11:54:55 +0200 Subject: [PATCH 14/14] use the epsidoeId for the next epsiode in PlayerViewModel --- .../ui/activity/player/PlayerActivity.kt | 12 ++-- .../ui/activity/player/PlayerViewModel.kt | 55 ++++++++++--------- .../ui/components/EpisodesListPlayer.kt | 2 +- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index a1307d8..f3e2008 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -118,13 +118,13 @@ class PlayerActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - // when the intent changed, lead the new media and play it + // when the intent changed, load the new media and play it intent?.let { model.loadMedia( it.getIntExtra(getString(R.string.intent_media_id), 0), it.getIntExtra(getString(R.string.intent_episode_id), 0) ) - model.playEpisode(model.currentEpisode, replace = true) + model.playEpisode(model.currentEpisode.mediaId, replace = true) } } @@ -206,14 +206,14 @@ class PlayerActivity : AppCompatActivity() { else -> View.VISIBLE } - if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) { + if (state == ExoPlayer.STATE_ENDED && model.nextEpisodeId != null && Preferences.autoplay) { playNextEpisode() } } }) // start playing the current episode, after all needed player components have been initialized - model.playEpisode(model.currentEpisode, true) + model.playEpisode(model.currentEpisode.mediaId, true) } @SuppressLint("ClickableViewAccessibility") @@ -277,7 +277,7 @@ class PlayerActivity : AppCompatActivity() { // if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip: // show next ep button if (remainingTime in 1..20000) { - if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) { + if (!btnNextEpIsVisible && model.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) { showButtonNextEp() } } else if (btnNextEpIsVisible) { @@ -335,7 +335,7 @@ class PlayerActivity : AppCompatActivity() { exo_text_title.text = model.getMediaTitle() // hide the next ep button, if there is none - button_next_ep_c.visibility = if (model.nextEpisode == null) { + button_next_ep_c.visibility = if (model.nextEpisodeId == null) { View.GONE } else { View.VISIBLE 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 194110d..ca14e0f 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 @@ -29,9 +29,6 @@ import kotlin.collections.ArrayList * PlayerViewModel handles all stuff related to media/episodes. * When currentEpisode is changed the player will start playing it (not initial media), * the next episode will be update and the callback is handled. - * - * TODO rework don't use episodes for everything, use media instead - * this is a major rework of the AoDParser/Player/Media architecture */ class PlayerViewModel(application: Application) : AndroidViewModel(application) { @@ -44,16 +41,16 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) var media: AoDMedia = AoDMediaNone internal set - var currentEpisode = AoDEpisodeNone - internal set - var nextEpisode: AoDEpisode? = null + var mediaMeta: Meta? = null internal set var tmdbTVSeason: TMDBTVSeason? =null internal set - var mediaMeta: Meta? = null + var currentEpisode = AoDEpisodeNone internal set var currentEpisodeMeta: EpisodeMeta? = null internal set + var nextEpisodeId: Int? = null + internal set var currentLanguage: Locale = Locale.ROOT internal set @@ -97,7 +94,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) } currentEpisode = media.getEpisodeById(episodeId) - nextEpisode = selectNextEpisode() + nextEpisodeId = selectNextEpisode() currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language } @@ -125,33 +122,37 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) /** * play the next episode, if nextEpisode is not null */ - fun playNextEpisode() = nextEpisode?.let { it -> + fun playNextEpisode() = nextEpisodeId?.let { it -> playEpisode(it, replace = true) } /** - * set currentEpisode to the param episode and start playing it - * update nextEpisode to reflect the change + * Set currentEpisode and start playing it. + * Update nextEpisode to reflect the change and update + * the watched state for the now playing episode. * - * updateWatchedState for the next (now current) episode + * @param episodeId The aod media id of the episode to play. + * @param replace (default = false) + * @param seekPosition The seek position for the episode (default = 0). */ - fun playEpisode(episode: AoDEpisode, replace: Boolean = false, seekPosition: Long = 0) { - val preferredStream = episode.getPreferredStream(currentLanguage) - currentLanguage = preferredStream.language // update current language, since it may have changed - currentEpisode = episode - nextEpisode = selectNextEpisode() - currentEpisodeMeta = getEpisodeMetaByAoDMediaId(episode.mediaId) - currentEpisodeChangedListener.forEach { it() } // update player gui (title) + fun playEpisode(episodeId: Int, replace: Boolean = false, seekPosition: Long = 0) { + currentEpisode = media.getEpisodeById(episodeId) + currentLanguage = currentEpisode.getPreferredStream(currentLanguage).language + currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) + nextEpisodeId = selectNextEpisode() + + // update player gui (title, next ep button) after nextEpisodeId has been set + currentEpisodeChangedListener.forEach { it() } val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( - MediaItem.fromUri(Uri.parse(preferredStream.url)) + MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(currentLanguage).url)) ) playMedia(mediaSource, replace, seekPosition) // if episodes has not been watched, mark as watched - if (!episode.watched) { + if (!currentEpisode.watched) { viewModelScope.launch { - AoDParser.markAsWatched(media.aodId, episode.mediaId) + AoDParser.markAsWatched(media.aodId, currentEpisode.mediaId) } } } @@ -198,11 +199,13 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) } /** - * Based on the current episodeId, get the next episode. If there is no next - * episode, return null + * Based on the current episodes index, get the next episode. + * @return The next episode or null if there is none. */ - private fun selectNextEpisode(): AoDEpisode? { - return media.playlist.firstOrNull { it.index > media.getEpisodeById(currentEpisode.mediaId).index } + private fun selectNextEpisode(): Int? { + return media.playlist.firstOrNull { + it.index > media.getEpisodeById(currentEpisode.mediaId).index + }?.mediaId } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt index 9a7874d..13a6d40 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt @@ -31,7 +31,7 @@ class EpisodesListPlayer @JvmOverloads constructor( adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.playlist, model.tmdbTVSeason?.episodes) adapterRecEpisodes.onImageClick = { _, position -> (this.parent as ViewGroup).removeView(this) - model.playEpisode(model.media.playlist[position], replace = true) + model.playEpisode(model.media.playlist[position].mediaId, replace = true) } adapterRecEpisodes.currentSelected = model.currentEpisode.index -- 2.44.0