Implement media fragment for tv shows
This commit is contained in:
		| @ -11,12 +11,13 @@ import kotlinx.coroutines.* | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.decodeFromString | ||||
| import kotlinx.serialization.json.Json | ||||
| import java.util.* | ||||
|  | ||||
| private val json = Json { ignoreUnknownKeys = true } | ||||
|  | ||||
| object Crunchyroll { | ||||
|  | ||||
|     private val baseUrl = "https://beta-api.crunchyroll.com" | ||||
|     private const val baseUrl = "https://beta-api.crunchyroll.com" | ||||
|  | ||||
|     private var accessToken = "" | ||||
|     private var tokenType = "" | ||||
| @ -25,9 +26,14 @@ object Crunchyroll { | ||||
|     private var signature = "" | ||||
|     private var keyPairID = "" | ||||
|  | ||||
|     // TODO temp helper vary | ||||
|     var locale = "${Locale.GERMANY.language}-${Locale.GERMANY.country}" | ||||
|     var country = Locale.GERMANY.country | ||||
|  | ||||
|     val browsingCache = arrayListOf<Item>() | ||||
|  | ||||
|     fun login(username: String, password: String): Boolean = runBlocking { | ||||
|         val tokenEndpoint = "/auth/v1/token" | ||||
|  | ||||
|         val formData = listOf( | ||||
|             "username" to username, | ||||
|             "password" to password, | ||||
| @ -63,9 +69,15 @@ object Crunchyroll { | ||||
|     } | ||||
|  | ||||
|     // TODO get/post difference | ||||
|     private suspend fun request(endpoint: String, params: Parameters = listOf()): Result<FuelJson, FuelError> = coroutineScope { | ||||
|     private suspend fun request( | ||||
|         endpoint: String, | ||||
|         params: Parameters = listOf(), | ||||
|         url: String = "" | ||||
|     ): Result<FuelJson, FuelError> = coroutineScope { | ||||
|         val path = if (url.isEmpty()) "$baseUrl$endpoint" else url | ||||
|  | ||||
|         return@coroutineScope (Dispatchers.IO) { | ||||
|             val (request, response, result) = Fuel.get("$baseUrl$endpoint", params) | ||||
|             val (request, response, result) = Fuel.get(path, params) | ||||
|                 .header("Authorization", "$tokenType $accessToken") | ||||
|                 .responseJson() | ||||
|  | ||||
| @ -77,42 +89,6 @@ object Crunchyroll { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // TESTING | ||||
|  | ||||
|  | ||||
|     // TODO sort_by, default alphabetical, n, locale de-DE, categories | ||||
|     /** | ||||
|      * Browse the media available on crunchyroll. | ||||
|      * | ||||
|      * @param sortBy | ||||
|      * @param n Number of items to return, defaults to 10 | ||||
|      * | ||||
|      * @return A **[BrowseResult]** object is returned. | ||||
|      */ | ||||
|     suspend fun browse(sortBy: SortBy = SortBy.ALPHABETICAL, n: Int = 10): BrowseResult { | ||||
|         val browseEndpoint = "/content/v1/browse" | ||||
|         val parameters = listOf("sort_by" to sortBy.str, "n" to n) | ||||
|  | ||||
|         val result = request(browseEndpoint, parameters) | ||||
|  | ||||
| //        val browseResult = json.decodeFromString<BrowseResult>(result.component1()?.obj()?.toString()!!) | ||||
| //        println(browseResult.items.size) | ||||
|  | ||||
|         return json.decodeFromString(result.component1()?.obj()?.toString()!!) | ||||
|     } | ||||
|  | ||||
|     // TODO | ||||
|     suspend fun search() { | ||||
|         val searchEndpoint = "/content/v1/search" | ||||
|         val result = request(searchEndpoint) | ||||
|  | ||||
|         println("${result.component1()?.obj()?.get("total")}") | ||||
|  | ||||
|         val test = json.decodeFromString<BrowseResult>(result.component1()?.obj()?.toString()!!) | ||||
|         println(test.items.size) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Retrieve the identifiers necessary for streaming. If the identifiers are | ||||
|      * retrieved, set the corresponding global var. The identifiers are valid for 24h. | ||||
| @ -132,4 +108,108 @@ object Crunchyroll { | ||||
|         println("keyPairID: $keyPairID") | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // TODO locale de-DE, categories | ||||
|     /** | ||||
|      * Browse the media available on crunchyroll. | ||||
|      * | ||||
|      * @param sortBy | ||||
|      * @param n Number of items to return, defaults to 10 | ||||
|      * | ||||
|      * @return A **[BrowseResult]** object is returned. | ||||
|      */ | ||||
|     suspend fun browse(sortBy: SortBy = SortBy.ALPHABETICAL, n: Int = 10): BrowseResult { | ||||
|         val browseEndpoint = "/content/v1/browse" | ||||
|         val parameters = listOf("sort_by" to sortBy.str, "n" to n) | ||||
|  | ||||
|         val result = request(browseEndpoint, parameters) | ||||
|         val browseResult = result.component1()?.obj()?.let { | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneBrowseResult | ||||
|  | ||||
|         // add results to cache TODO improve | ||||
|         browsingCache.clear() | ||||
|         browsingCache.addAll(browseResult.items) | ||||
|  | ||||
|         return browseResult | ||||
|     } | ||||
|  | ||||
|     // // TODO locale de-DE, type | ||||
|     suspend fun search(query: String, n: Int = 10) { | ||||
|         val searchEndpoint = "/content/v1/search" | ||||
|         val parameters = listOf("q" to query, "n" to n) | ||||
|  | ||||
|         val result = request(searchEndpoint, parameters) | ||||
|         println("${result.component1()?.obj()?.get("total")}") | ||||
|  | ||||
|         val test = json.decodeFromString<BrowseResult>(result.component1()?.obj()?.toString()!!) | ||||
|         println(test.items.size) | ||||
|  | ||||
|         // TODO return | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * series id == crunchyroll id? | ||||
|      */ | ||||
|     suspend fun series(seriesId: String): Series { | ||||
|         val seriesEndpoint = "/cms/v2/$country/M3/crunchyroll/series/$seriesId" | ||||
|         val parameters = listOf( | ||||
|             "locale" to locale, | ||||
|             "Signature" to signature, | ||||
|             "Policy" to policy, | ||||
|             "Key-Pair-Id" to keyPairID | ||||
|         ) | ||||
|  | ||||
|         val result = request(seriesEndpoint, parameters) | ||||
|  | ||||
|         return result.component1()?.obj()?.let { | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneSeries | ||||
|     } | ||||
|  | ||||
|     suspend fun seasons(seriesId: String): Seasons { | ||||
|         val episodesEndpoint = "/cms/v2/$country/M3/crunchyroll/seasons" | ||||
|         val parameters = listOf( | ||||
|             "series_id" to seriesId, | ||||
|             "locale" to locale, | ||||
|             "Signature" to signature, | ||||
|             "Policy" to policy, | ||||
|             "Key-Pair-Id" to keyPairID | ||||
|         ) | ||||
|  | ||||
|         val result = request(episodesEndpoint, parameters) | ||||
|  | ||||
|         return result.component1()?.obj()?.let { | ||||
|             println(it) | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneSeasons | ||||
|     } | ||||
|  | ||||
|     suspend fun episodes(seasonId: String): Episodes { | ||||
|         val episodesEndpoint = "/cms/v2/$country/M3/crunchyroll/episodes" | ||||
|         val parameters = listOf( | ||||
|             "season_id" to seasonId, | ||||
|             "locale" to locale, | ||||
|             "Signature" to signature, | ||||
|             "Policy" to policy, | ||||
|             "Key-Pair-Id" to keyPairID | ||||
|         ) | ||||
|  | ||||
|         val result = request(episodesEndpoint, parameters) | ||||
|  | ||||
|         return result.component1()?.obj()?.let { | ||||
|             println(it) | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NoneEpisodes | ||||
|     } | ||||
|  | ||||
|     suspend fun playback(url: String): Playback { | ||||
|         val result = request("", url = url) | ||||
|  | ||||
|         return result.component1()?.obj()?.let { | ||||
|             println(it) | ||||
|             json.decodeFromString(it.toString()) | ||||
|         } ?: NonePlayback | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -1,5 +1,6 @@ | ||||
| package org.mosad.teapod.parser.crunchyroll | ||||
|  | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| /** | ||||
| @ -26,9 +27,141 @@ data class Item( | ||||
|     // TODO metadata etc. | ||||
| ) | ||||
|  | ||||
| val NoneItem = Item("", "", "", "", "", Images(listOf(), listOf())) | ||||
| val NoneBrowseResult = BrowseResult(0, listOf()) | ||||
|  | ||||
| @Serializable | ||||
| data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<List<Poster>>) | ||||
| // crunchyroll why? | ||||
|  | ||||
| @Serializable | ||||
| data class Poster(val height: Int, val width: Int, val source: String, val type: String) | ||||
| data class Poster(val height: Int, val width: Int, val source: String, val type: String) | ||||
|  | ||||
| /** | ||||
|  * Series return type | ||||
|  */ | ||||
| @Serializable | ||||
| data class Series( | ||||
|     val id: String, | ||||
|     val title: String, | ||||
|     val description: String, | ||||
|     val images: Images | ||||
| ) | ||||
| val NoneSeries = Series("", "", "", Images(listOf(), listOf())) | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Seasons data type | ||||
|  */ | ||||
| @Serializable | ||||
| data class Seasons(val total: Int, val items: List<Season>) | ||||
|  | ||||
| @Serializable | ||||
| data class Season( | ||||
|     val id: String, | ||||
|     val title: String, | ||||
|     val series_id: String, | ||||
|     val season_number: Int | ||||
| ) | ||||
|  | ||||
| val NoneSeasons = Seasons(0, listOf()) | ||||
|  | ||||
| /** | ||||
|  * Episodes data type | ||||
|  */ | ||||
| @Serializable | ||||
| data class Episodes(val total: Int, val items: List<Episode>) | ||||
|  | ||||
| @Serializable | ||||
| data class Episode( | ||||
|     @SerialName("id") val id: String, | ||||
|     @SerialName("title") val title: String, | ||||
|     @SerialName("series_id") val seriesId: String, | ||||
|     @SerialName("season_title") val seasonTitle: String, | ||||
|     @SerialName("season_id") val seasonId: String, | ||||
|     @SerialName("season_number") val seasonNumber: Int, | ||||
|     @SerialName("episode") val episode: String, | ||||
|     @SerialName("episode_number") val episodeNumber: Int, | ||||
|     @SerialName("description") val description: String, | ||||
|     @SerialName("next_episode_id") val nextEpisodeId: String = "", // use default value since the field is optional | ||||
|     @SerialName("next_episode_title") val nextEpisodeTitle: String = "", // use default value since the field is optional | ||||
|     @SerialName("is_subbed") val isSubbed: Boolean, | ||||
|     @SerialName("is_dubbed") val isDubbed: Boolean, | ||||
|     @SerialName("images") val images: Thumbnail, | ||||
|     @SerialName("duration_ms") val durationMs: Int, | ||||
|     @SerialName("playback") val playback: String, | ||||
| ) | ||||
|  | ||||
| @Serializable | ||||
| data class Thumbnail( | ||||
|     @SerialName("thumbnail") val thumbnail: List<List<Poster>> | ||||
| ) | ||||
|  | ||||
| val NoneEpisodes = Episodes(0, listOf()) | ||||
| val NoneEpisode = Episode( | ||||
|     id = "", | ||||
|     title = "", | ||||
|     seriesId = "", | ||||
|     seasonId = "", | ||||
|     seasonTitle = "", | ||||
|     seasonNumber = 0, | ||||
|     episode = "", | ||||
|     episodeNumber = 0, | ||||
|     description = "", | ||||
|     nextEpisodeId = "", | ||||
|     nextEpisodeTitle = "", | ||||
|     isSubbed = false, | ||||
|     isDubbed = false, | ||||
|     images = Thumbnail(listOf()), | ||||
|     durationMs = 0, | ||||
|     playback = "" | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * Playback/stream data type | ||||
|  */ | ||||
| @Serializable | ||||
| data class Playback( | ||||
|     @SerialName("audio_locale") val audioLocale: String, | ||||
|     @SerialName("subtitles") val subtitles: Map<String, Subtitle>, | ||||
|     @SerialName("streams") val streams: Streams, | ||||
| ) | ||||
|  | ||||
| @Serializable | ||||
| data class Subtitle( | ||||
|     @SerialName("locale") val locale: String, | ||||
|     @SerialName("url") val url: String, | ||||
|     @SerialName("format") val format: String, | ||||
| ) | ||||
|  | ||||
| @Serializable | ||||
| data class Streams( | ||||
|     @SerialName("adaptive_dash") val adaptive_dash: Map<String, Stream>, | ||||
|     @SerialName("adaptive_hls") val adaptive_hls: Map<String, Stream>, | ||||
|     @SerialName("download_hls") val download_hls: Map<String, Stream>, | ||||
|     @SerialName("drm_adaptive_dash") val drm_adaptive_dash: Map<String, Stream>, | ||||
|     @SerialName("drm_adaptive_hls") val drm_adaptive_hls: Map<String, Stream>, | ||||
|     @SerialName("drm_download_hls") val drm_download_hls: Map<String, Stream>, | ||||
|     @SerialName("trailer_dash") val trailer_dash: Map<String, Stream>, | ||||
|     @SerialName("trailer_hls") val trailer_hls: Map<String, Stream>, | ||||
|     @SerialName("vo_adaptive_dash") val vo_adaptive_dash: Map<String, Stream>, | ||||
|     @SerialName("vo_adaptive_hls") val vo_adaptive_hls: Map<String, Stream>, | ||||
|     @SerialName("vo_drm_adaptive_dash") val vo_drm_adaptive_dash: Map<String, Stream>, | ||||
|     @SerialName("vo_drm_adaptive_hls") val vo_drm_adaptive_hls: Map<String, Stream>, | ||||
| ) | ||||
|  | ||||
| @Serializable | ||||
| data class Stream( | ||||
|     @SerialName("hardsub_locale") val hardsubLocale: String, | ||||
|     @SerialName("url") val url: String, | ||||
|     @SerialName("vcodec") val vcodec: String, | ||||
| ) | ||||
|  | ||||
| val NonePlayback = Playback( | ||||
|     "", | ||||
|     mapOf(), | ||||
|     Streams( | ||||
|         mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), | ||||
|         mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| @ -47,6 +47,7 @@ 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 java.util.* | ||||
| import kotlin.system.measureTimeMillis | ||||
|  | ||||
| class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener { | ||||
| @ -138,7 +139,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen | ||||
|             // start the initial loading | ||||
|             val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope")) | ||||
|                 .async { | ||||
|                     launch { AoDParser.initialLoading() } | ||||
|                     launch { MetaDBController.list() } | ||||
|                 } | ||||
|  | ||||
| @ -209,9 +209,9 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen | ||||
|     /** | ||||
|      * start the player as new activity | ||||
|      */ | ||||
|     fun startPlayer(mediaId: Int, episodeId: Int) { | ||||
|     fun startPlayer(seasonId: String, episodeId: String) { | ||||
|         val intent = Intent(this, PlayerActivity::class.java).apply { | ||||
|             putExtra(getString(R.string.intent_media_id), mediaId) | ||||
|             putExtra(getString(R.string.intent_season_id), seasonId) | ||||
|             putExtra(getString(R.string.intent_episode_id), episodeId) | ||||
|         } | ||||
|         startActivity(intent) | ||||
|  | ||||
| @ -99,7 +99,7 @@ class HomeFragment : Fragment() { | ||||
|                 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) | ||||
|                 //(activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId) // TODO | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -117,27 +117,27 @@ class HomeFragment : Fragment() { | ||||
|         } | ||||
|  | ||||
|         binding.textHighlightInfo.setOnClickListener { | ||||
|             activity?.showFragment(MediaFragment(highlightMedia.id)) | ||||
|             activity?.showFragment(MediaFragment("")) | ||||
|         } | ||||
|  | ||||
|         adapterMyList.onItemClick = { id, _ -> | ||||
|             activity?.showFragment(MediaFragment(id)) | ||||
|             activity?.showFragment(MediaFragment("")) //(mediaId)) | ||||
|         } | ||||
|  | ||||
|         adapterNewEpisodes.onItemClick = { id, _ -> | ||||
|             activity?.showFragment(MediaFragment(id)) | ||||
|             activity?.showFragment(MediaFragment("")) //(mediaId)) | ||||
|         } | ||||
|  | ||||
|         adapterNewSimulcasts.onItemClick = { id, _ -> | ||||
|             activity?.showFragment(MediaFragment(id)) | ||||
|             activity?.showFragment(MediaFragment("")) //(mediaId)) | ||||
|         } | ||||
|  | ||||
|         adapterNewTitles.onItemClick = { id, _ -> | ||||
|             activity?.showFragment(MediaFragment(id)) | ||||
|             activity?.showFragment(MediaFragment("")) //(mediaId)) | ||||
|         } | ||||
|  | ||||
|         adapterTopTen.onItemClick = { id, _ -> | ||||
|             activity?.showFragment(MediaFragment(id)) | ||||
|             activity?.showFragment(MediaFragment("")) //(mediaId)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -35,13 +35,13 @@ class LibraryFragment : Fragment() { | ||||
|                 // crunchy testing TODO implement lazy loading | ||||
|                 val results = Crunchyroll.browse(n = 50) | ||||
|                 val list = results.items.mapIndexed { index, item -> | ||||
|                     ItemMedia(index, item.title, item.images.poster_wide[0][0].source) | ||||
|                     ItemMedia(index, item.title, item.images.poster_wide[0][0].source, idStr = item.id) | ||||
|                 } | ||||
|  | ||||
|  | ||||
|                 adapter = MediaItemAdapter(list) | ||||
|                 adapter.onItemClick = { mediaId, _ -> | ||||
|                     activity?.showFragment(MediaFragment(mediaId)) | ||||
|                 adapter.onItemClick = { mediaIdStr, _ -> | ||||
|                     activity?.showFragment(MediaFragment(mediaIdStr = mediaIdStr)) | ||||
|                 } | ||||
|  | ||||
|                 binding.recyclerMediaLibrary.adapter = adapter | ||||
|  | ||||
| @ -15,11 +15,12 @@ import androidx.viewpager2.adapter.FragmentStateAdapter | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import com.google.android.material.appbar.AppBarLayout | ||||
| import com.google.android.material.tabs.TabLayoutMediator | ||||
| import jp.wasabeef.glide.transformations.BlurTransformation | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.FragmentMediaBinding | ||||
| import org.mosad.teapod.parser.crunchyroll.Item | ||||
| import org.mosad.teapod.parser.crunchyroll.NoneItem | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
| @ -32,7 +33,7 @@ import org.mosad.teapod.util.tmdb.TMDBApiController | ||||
|  * Note: the fragment is created only once, when selecting a similar title etc. | ||||
|  * therefore fragments may be not empty and model may be the old one | ||||
|  */ | ||||
| class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
| class MediaFragment(private val mediaIdStr: String, mediaCr: Item = NoneItem) : Fragment() { | ||||
|  | ||||
|     private lateinit var binding: FragmentMediaBinding | ||||
|     private lateinit var pagerAdapter: FragmentStateAdapter | ||||
| @ -55,16 +56,17 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|         // fix material components issue #1878, if more tabs are added increase | ||||
|         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) { | ||||
|                 getString(R.string.episodes) | ||||
|             } else { | ||||
|                 getString(R.string.similar_titles) | ||||
|             } | ||||
|         }.attach() | ||||
|         // TODO implement for cr media items | ||||
| //        TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position -> | ||||
| //            tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) { | ||||
| //                getString(R.string.episodes) | ||||
| //            } else { | ||||
| //                getString(R.string.similar_titles) | ||||
| //            } | ||||
| //        }.attach() | ||||
|  | ||||
|         lifecycleScope.launch { | ||||
|             model.load(mediaId) // load the streams and tmdb for the selected media | ||||
|             model.loadCrunchy(mediaIdStr) | ||||
|  | ||||
|             updateGUI() | ||||
|             initActions() | ||||
| @ -86,9 +88,9 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|     private fun updateGUI() = with(model) { | ||||
|         // generic gui | ||||
|         val backdropUrl = tmdbResult?.backdropPath?.let { TMDBApiController.imageUrl + it } | ||||
|             ?: media.posterURL | ||||
|             ?: mediaCrunchy.images.poster_wide[0][2].source | ||||
|         val posterUrl = tmdbResult?.posterPath?.let { TMDBApiController.imageUrl + it } | ||||
|             ?: media.posterURL | ||||
|             ?: mediaCrunchy.images.poster_tall[0][2].source | ||||
|  | ||||
|         // load poster and backdrop | ||||
|         Glide.with(requireContext()).load(posterUrl) | ||||
| @ -98,12 +100,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|             .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) | ||||
|             .into(binding.imageBackdrop) | ||||
|  | ||||
|         binding.textTitle.text = media.title | ||||
|         binding.textYear.text = media.year.toString() | ||||
|         binding.textAge.text = media.age.toString() | ||||
|         binding.textOverview.text = media.shortText | ||||
|         binding.textTitle.text = mediaCrunchy.title | ||||
|         //binding.textYear.text = media.year.toString() // TODO | ||||
|         //binding.textAge.text = media.age.toString() // TODO | ||||
|         binding.textOverview.text = mediaCrunchy.description | ||||
|  | ||||
|         // set "my list" indicator | ||||
|         // TODO set "my list" indicator | ||||
|         if (StorageController.myList.contains(media.aodId)) { | ||||
|             Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) | ||||
|         } else { | ||||
| @ -116,19 +118,19 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|         pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize) | ||||
|  | ||||
|         // specific gui | ||||
|         if (media.type == MediaType.TVSHOW) { | ||||
|             // get next episode | ||||
|             nextEpisodeId = media.playlist.firstOrNull{ !it.watched }?.mediaId | ||||
|                 ?: media.playlist.first().mediaId | ||||
|         if (mediaCrunchy.type == MediaType.TVSHOW.str) { | ||||
|             // TODO get next episode | ||||
| //            nextEpisodeId = media.playlist.firstOrNull{ !it.watched }?.mediaId | ||||
| //                ?: media.playlist.first().mediaId | ||||
|  | ||||
|             // title is the next episodes title | ||||
|             binding.textTitle.text = media.getEpisodeById(nextEpisodeId).title | ||||
|             // TODO title is the next episodes title | ||||
| //            binding.textTitle.text = media.getEpisodeById(nextEpisodeId).title | ||||
|  | ||||
|             // episodes count | ||||
|             binding.textEpisodesOrRuntime.text = resources.getQuantityString( | ||||
|                 R.plurals.text_episodes_count, | ||||
|                 media.playlist.size, | ||||
|                 media.playlist.size | ||||
|                 episodesCrunchy.total, | ||||
|                 episodesCrunchy.total | ||||
|             ) | ||||
|  | ||||
|             // episodes | ||||
| @ -170,8 +172,8 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|     private fun initActions() = with(model) { | ||||
|         binding.buttonPlay.setOnClickListener { | ||||
|             when (media.type) { | ||||
|                 MediaType.MOVIE -> playEpisode(media.playlist.first().mediaId) | ||||
|                 MediaType.TVSHOW -> playEpisode(nextEpisodeId) | ||||
|                 //MediaType.MOVIE -> playEpisode(media.playlist.first().mediaId) // TODO | ||||
|                 //MediaType.TVSHOW -> playEpisode(nextEpisodeId) // TODO | ||||
|                 else -> Log.e(javaClass.name, "Wrong Type: ${media.type}") | ||||
|             } | ||||
|         } | ||||
| @ -198,11 +200,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(episodeId: Int) { | ||||
|         (activity as MainActivity).startPlayer(model.media.aodId, episodeId) | ||||
|     private fun playEpisode(seasonId: String, episodeId: String) { | ||||
|         (activity as MainActivity).startPlayer(seasonId, episodeId) | ||||
|         Log.d(javaClass.name, "Started Player with  episodeId: $episodeId") | ||||
|  | ||||
|         model.updateNextEpisode(episodeId) // set the correct next episode | ||||
|         //model.updateNextEpisode(episodeId) // set the correct next episode | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|  | ||||
| @ -27,13 +27,14 @@ class MediaFragmentEpisodes : Fragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.media.playlist, model.tmdbTVSeason?.episodes) | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.episodesCrunchy, model.tmdbTVSeason?.episodes) | ||||
|         binding.recyclerEpisodes.adapter = adapterRecEpisodes | ||||
|  | ||||
|         // set onItemClick only in adapter is initialized | ||||
|         if (this::adapterRecEpisodes.isInitialized) { | ||||
|             adapterRecEpisodes.onImageClick = { _, position -> | ||||
|                 playEpisode(model.media.playlist[position].mediaId) | ||||
|             adapterRecEpisodes.onImageClick = { seasonId, episodeId -> | ||||
|                 println("TODO playback episode $episodeId (season: $seasonId)") | ||||
|                 playEpisode(seasonId, episodeId) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @ -50,11 +51,11 @@ class MediaFragmentEpisodes : Fragment() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun playEpisode(episodeId: Int) { | ||||
|         (activity as MainActivity).startPlayer(model.media.aodId, episodeId) | ||||
|     private fun playEpisode(seasonId: String, episodeId: String) { | ||||
|         (activity as MainActivity).startPlayer(seasonId, episodeId) | ||||
|         Log.d(javaClass.name, "Started Player with  episodeId: $episodeId") | ||||
|  | ||||
|         model.updateNextEpisode(episodeId) // set the correct next episode | ||||
|         //model.updateNextEpisode(episodeId) // set the correct next episode | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -34,7 +34,7 @@ class MediaFragmentSimilar : Fragment()  { | ||||
|         // set onItemClick only in adapter is initialized | ||||
|         if (this::adapterSimilar.isInitialized) { | ||||
|             adapterSimilar.onItemClick = { mediaId, _ -> | ||||
|                 activity?.showFragment(MediaFragment(mediaId)) | ||||
|                 activity?.showFragment(MediaFragment("")) //(mediaId)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -33,7 +33,7 @@ class SearchFragment : Fragment() { | ||||
|                     adapter = MediaItemAdapter(AoDParser.guiMediaList) | ||||
|                     adapter!!.onItemClick = { mediaId, _ -> | ||||
|                         binding.searchText.clearFocus() | ||||
|                         activity?.showFragment(MediaFragment(mediaId)) | ||||
|                         activity?.showFragment(MediaFragment("")) //(mediaId)) | ||||
|                     } | ||||
|  | ||||
|                     binding.recyclerMediaSearch.adapter = adapter | ||||
|  | ||||
| @ -4,6 +4,8 @@ import android.app.Application | ||||
| import android.util.Log | ||||
| import androidx.lifecycle.AndroidViewModel | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.parser.crunchyroll.* | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.util.* | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
| import org.mosad.teapod.util.tmdb.TMDBApiController | ||||
| @ -21,6 +23,13 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|     var nextEpisodeId = -1 | ||||
|         internal set | ||||
|  | ||||
|     var mediaCrunchy = NoneItem | ||||
|         internal set | ||||
|     var seasonsCrunchy = NoneSeasons | ||||
|         internal set | ||||
|     var episodesCrunchy = NoneEpisodes | ||||
|         internal set | ||||
|  | ||||
|     var tmdbResult: TMDBResult? = null // TODO rename | ||||
|         internal set | ||||
|     var tmdbTVSeason: TMDBTVSeason? =null | ||||
| @ -28,11 +37,45 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|     var mediaMeta: Meta? = null | ||||
|         internal set | ||||
|  | ||||
|     suspend fun loadCrunchy(crunchyId: String) { | ||||
|         val tmdbApiController = TMDBApiController() | ||||
|  | ||||
|         println("loading crunchyroll media $crunchyId") | ||||
|  | ||||
|         // TODO info also in browse result item | ||||
|         mediaCrunchy = Crunchyroll.browsingCache.find { it -> | ||||
|             it.id == crunchyId | ||||
|         } ?: NoneItem | ||||
|         println("media: $mediaCrunchy") | ||||
|  | ||||
|         // load seasons | ||||
|         seasonsCrunchy = Crunchyroll.seasons(crunchyId) | ||||
|         println("media: $seasonsCrunchy") | ||||
|  | ||||
|         // load first season | ||||
|         episodesCrunchy = Crunchyroll.episodes(seasonsCrunchy.items.first().id) | ||||
|         println("media: $episodesCrunchy") | ||||
|  | ||||
|  | ||||
|  | ||||
|         // TODO check if metaDB knows the title | ||||
|  | ||||
|         // use tmdb search to get media info TODO media type is hardcoded, use type info from browse result once implemented | ||||
|         mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media | ||||
|         val tmdbId = tmdbApiController.search(stripTitleInfo(mediaCrunchy.title), MediaType.TVSHOW) | ||||
|  | ||||
|         tmdbResult = when (MediaType.TVSHOW) { | ||||
|             MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) | ||||
|             MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) | ||||
|             else -> null | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * set media, tmdb and nextEpisode | ||||
|      * TODO run aod and tmdb load parallel | ||||
|      */ | ||||
|     suspend fun load(aodId: Int) { | ||||
|     suspend fun loadAoD(aodId: Int) { | ||||
|         val tmdbApiController = TMDBApiController() | ||||
|         media = AoDParser.getMediaById(aodId) | ||||
|  | ||||
|  | ||||
| @ -58,8 +58,8 @@ class PlayerActivity : AppCompatActivity() { | ||||
|         hideBars() // Initial hide the bars | ||||
|  | ||||
|         model.loadMedia( | ||||
|             intent.getIntExtra(getString(R.string.intent_media_id), 0), | ||||
|             intent.getIntExtra(getString(R.string.intent_episode_id), 0) | ||||
|             intent.getStringExtra(getString(R.string.intent_season_id)) ?: "", | ||||
|             intent.getStringExtra(getString(R.string.intent_episode_id)) ?: "" | ||||
|         ) | ||||
|         model.currentEpisodeChangedListener.add { onMediaChanged() } | ||||
|         gestureDetector = GestureDetectorCompat(this, PlayerGestureListener()) | ||||
| @ -121,8 +121,8 @@ class PlayerActivity : AppCompatActivity() { | ||||
|         // 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) | ||||
|                 it.getStringExtra(getString(R.string.intent_season_id)) ?: "", | ||||
|                 it.getStringExtra(getString(R.string.intent_episode_id)) ?: "" | ||||
|             ) | ||||
|             model.playEpisode(model.currentEpisode.mediaId, replace = true) | ||||
|         } | ||||
|  | ||||
| @ -18,6 +18,10 @@ import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.parser.crunchyroll.Crunchyroll | ||||
| import org.mosad.teapod.parser.crunchyroll.NoneEpisode | ||||
| import org.mosad.teapod.parser.crunchyroll.NoneEpisodes | ||||
| import org.mosad.teapod.parser.crunchyroll.NonePlayback | ||||
| import org.mosad.teapod.preferences.Preferences | ||||
| import org.mosad.teapod.util.* | ||||
| import org.mosad.teapod.util.tmdb.TMDBApiController | ||||
| @ -54,6 +58,13 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) | ||||
|     var currentLanguage: Locale = Locale.ROOT | ||||
|         internal set | ||||
|  | ||||
|     var episodesCrunchy = NoneEpisodes | ||||
|         internal set | ||||
|     var currentEpisodeCr = NoneEpisode | ||||
|         internal set | ||||
|     var currentPlaybackCr = NonePlayback | ||||
|         internal set | ||||
|  | ||||
|     init { | ||||
|         initMediaSession() | ||||
|     } | ||||
| @ -78,10 +89,17 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) | ||||
|         mediaSession.isActive = true | ||||
|     } | ||||
|  | ||||
|     fun loadMedia(mediaId: Int, episodeId: Int) { | ||||
|     fun loadMedia(seasonId: String, episodeId: String) { | ||||
|         runBlocking { | ||||
|             media = AoDParser.getMediaById(mediaId) | ||||
|             mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached | ||||
|             episodesCrunchy = Crunchyroll.episodes(seasonId) | ||||
|             //mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached | ||||
|  | ||||
|             currentEpisodeCr = episodesCrunchy.items.find { episode -> | ||||
|                 episode.id == episodeId | ||||
|             } ?: NoneEpisode | ||||
|             println("loading playback ${currentEpisodeCr.playback}") | ||||
|  | ||||
|             currentPlaybackCr = Crunchyroll.playback(currentEpisodeCr.playback) | ||||
|         } | ||||
|  | ||||
|         // run async as it should be loaded by the time the episodes a | ||||
| @ -93,8 +111,6 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         currentEpisode = media.getEpisodeById(episodeId) | ||||
|         nextEpisodeId = selectNextEpisode() | ||||
|         currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) | ||||
|         currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language | ||||
|     } | ||||
|  | ||||
| @ -3,10 +3,10 @@ package org.mosad.teapod.util | ||||
| import java.util.Locale | ||||
|  | ||||
| class DataTypes { | ||||
|     enum class MediaType { | ||||
|         OTHER, | ||||
|         MOVIE, | ||||
|         TVSHOW | ||||
|     enum class MediaType(val str: String) { | ||||
|         OTHER("other"), | ||||
|         MOVIE("movie"), // TODO | ||||
|         TVSHOW("series") | ||||
|     } | ||||
|  | ||||
|     enum class Theme(val str: String) { | ||||
| @ -37,7 +37,8 @@ data class ThirdPartyComponent( | ||||
| data class ItemMedia( | ||||
|     val id: Int, // aod path id | ||||
|     val title: String, | ||||
|     val posterUrl: String | ||||
|     val posterUrl: String, | ||||
|     val idStr: String = "" // crunchyroll id | ||||
| ) | ||||
|  | ||||
| // TODO replace playlist: List<AoDEpisode> with a map? | ||||
|  | ||||
| @ -4,19 +4,18 @@ import android.graphics.Color | ||||
| import android.graphics.drawable.ColorDrawable | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.bumptech.glide.Glide | ||||
| 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.AoDEpisode | ||||
| import org.mosad.teapod.parser.crunchyroll.Episodes | ||||
| import org.mosad.teapod.util.tmdb.TMDBTVEpisode | ||||
|  | ||||
| class EpisodeItemAdapter(private val episodes: List<AoDEpisode>, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() { | ||||
| class EpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() { | ||||
|  | ||||
|     var onImageClick: ((String, Int) -> Unit)? = null | ||||
|     var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null | ||||
|  | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder { | ||||
|         return EpisodeViewHolder(ItemEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false)) | ||||
| @ -24,52 +23,63 @@ class EpisodeItemAdapter(private val episodes: List<AoDEpisode>, private val tmd | ||||
|  | ||||
|     override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) { | ||||
|         val context = holder.binding.root.context | ||||
|         val ep = episodes[position] | ||||
|         val ep = episodes.items[position] | ||||
|  | ||||
|         val titleText = if (ep.hasDub()) { | ||||
|             context.getString(R.string.component_episode_title, ep.numberStr, ep.description) | ||||
|         val titleText = if (ep.isDubbed) { | ||||
|             context.getString(R.string.component_episode_title, ep.episode, ep.title) | ||||
|         } else { | ||||
|             context.getString(R.string.component_episode_title_sub, ep.numberStr, ep.description) | ||||
|             context.getString(R.string.component_episode_title_sub, ep.episode, ep.title) | ||||
|         } | ||||
|  | ||||
|         holder.binding.textEpisodeTitle.text = titleText | ||||
|         holder.binding.textEpisodeDesc.text = if (ep.shortDesc.isNotEmpty()) { | ||||
|             ep.shortDesc | ||||
|         holder.binding.textEpisodeDesc.text = if (ep.description.isNotEmpty()) { | ||||
|             ep.description | ||||
|         } else if (tmdbEpisodes != null && position < tmdbEpisodes.size){ | ||||
|             tmdbEpisodes[position].overview | ||||
|         } else { | ||||
|             "" | ||||
|         } | ||||
|  | ||||
|         if (ep.imageURL.isNotEmpty()) { | ||||
|             Glide.with(context).load(ep.imageURL) | ||||
|         // TODO is isNotEmpty() needed? | ||||
|         if (ep.images.thumbnail[0][0].source.isNotEmpty()) { | ||||
|             Glide.with(context).load(ep.images.thumbnail[0][0].source) | ||||
|                 .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) | ||||
|                 .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0))) | ||||
|                 .into(holder.binding.imageEpisode) | ||||
|         } | ||||
|  | ||||
|         if (ep.watched) { | ||||
|             holder.binding.imageWatched.setImageDrawable( | ||||
|                 ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) | ||||
|             ) | ||||
|         } else { | ||||
|             holder.binding.imageWatched.setImageDrawable(null) | ||||
|         } | ||||
|         // TODO | ||||
| //        if (ep.watched) { | ||||
| //            holder.binding.imageWatched.setImageDrawable( | ||||
| //                ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) | ||||
| //            ) | ||||
| //        } else { | ||||
| //            holder.binding.imageWatched.setImageDrawable(null) | ||||
| //        } | ||||
|         // disable watched icon until implemented | ||||
|         holder.binding.imageWatched.setImageDrawable(null) | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount(): Int { | ||||
|         return episodes.size | ||||
|         return episodes.items.size | ||||
|     } | ||||
|  | ||||
|     fun updateWatchedState(watched: Boolean, position: Int) { | ||||
|         // use getOrNull as there could be a index out of bound when running this in onResume() | ||||
|         episodes.getOrNull(position)?.watched = watched | ||||
|  | ||||
|         // TODO | ||||
|         //episodes.getOrNull(position)?.watched = watched | ||||
|     } | ||||
|  | ||||
|     inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) : RecyclerView.ViewHolder(binding.root) { | ||||
|     inner class EpisodeViewHolder(val binding: ItemEpisodeBinding) : | ||||
|         RecyclerView.ViewHolder(binding.root) { | ||||
|         init { | ||||
|             // on image click return the episode id and index (within the adapter) | ||||
|             binding.imageEpisode.setOnClickListener { | ||||
|                 onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition) | ||||
|                 onImageClick?.invoke( | ||||
|                     episodes.items[bindingAdapterPosition].seasonId, | ||||
|                     episodes.items[bindingAdapterPosition].id | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -12,7 +12,7 @@ import java.util.* | ||||
|  | ||||
| class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>(), Filterable { | ||||
|  | ||||
|     var onItemClick: ((Int, Int) -> Unit)? = null | ||||
|     var onItemClick: ((String, Int) -> Unit)? = null | ||||
|     private val filter = MediaFilter() | ||||
|     private var filteredMedia = initMedia.map { it.copy() } | ||||
|  | ||||
| @ -42,7 +42,7 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad | ||||
|     inner class MediaViewHolder(val binding: ItemMediaBinding) : RecyclerView.ViewHolder(binding.root) { | ||||
|         init { | ||||
|             binding.root.setOnClickListener { | ||||
|                 onItemClick?.invoke(filteredMedia[adapterPosition].id, adapterPosition) | ||||
|                 onItemClick?.invoke(filteredMedia[adapterPosition].idStr, adapterPosition) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -131,5 +131,6 @@ | ||||
|  | ||||
|     <!-- intents & states --> | ||||
|     <string name="intent_media_id" translatable="false">intent_media_id</string> | ||||
|     <string name="intent_season_id" translatable="false">intent_season_id</string> | ||||
|     <string name="intent_episode_id" translatable="false">intent_episode_id</string> | ||||
| </resources> | ||||
		Reference in New Issue
	
	Block a user