diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt index 204e2c1..91386cc 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt @@ -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() + 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 = coroutineScope { + private suspend fun request( + endpoint: String, + params: Parameters = listOf(), + url: String = "" + ): Result = 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(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(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(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 + } + } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt index 1ca5236..fb12c8b 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt @@ -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>, val poster_wide: List>) // crunchyroll why? @Serializable -data class Poster(val height: Int, val width: Int, val source: String, val type: String) \ No newline at end of file +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) + +@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) + +@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> +) + +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, + @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, + @SerialName("adaptive_hls") val adaptive_hls: Map, + @SerialName("download_hls") val download_hls: Map, + @SerialName("drm_adaptive_dash") val drm_adaptive_dash: Map, + @SerialName("drm_adaptive_hls") val drm_adaptive_hls: Map, + @SerialName("drm_download_hls") val drm_download_hls: Map, + @SerialName("trailer_dash") val trailer_dash: Map, + @SerialName("trailer_hls") val trailer_hls: Map, + @SerialName("vo_adaptive_dash") val vo_adaptive_dash: Map, + @SerialName("vo_adaptive_hls") val vo_adaptive_hls: Map, + @SerialName("vo_drm_adaptive_dash") val vo_drm_adaptive_dash: Map, + @SerialName("vo_drm_adaptive_hls") val vo_drm_adaptive_hls: Map, +) + +@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(), + ) +) 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 9386c17..48326d6 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 @@ -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) 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 ae7ddd4..25f12f7 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 @@ -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)) } } 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 03a31ce..cabe2b8 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 @@ -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 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 fb2cc42..87408e7 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 @@ -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 } /** 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 f2e9f58..a0985ce 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,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 } } \ 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 87195a1..c57770b 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 @@ -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)) } } } 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 a2943a9..08ea2ac 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 @@ -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 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 6f855d9..f6695b1 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 @@ -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) 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 f3e2008..82519e3 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 @@ -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) } 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 ca14e0f..c81c5d1 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 @@ -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 } 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 db662e5..83467fd 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -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 with a map? 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 3bd2df0..8baa776 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 @@ -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, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { +class EpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisodes: List?) : RecyclerView.Adapter() { - 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, 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 + ) } } } diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt index 2c23bcf..f5b862c 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt @@ -12,7 +12,7 @@ import java.util.* class MediaItemAdapter(private val initMedia: List) : RecyclerView.Adapter(), 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) : 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) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ce053ae..50246e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -131,5 +131,6 @@ intent_media_id + intent_season_id intent_episode_id \ No newline at end of file