Implement media fragment for tv shows

This commit is contained in:
Jannik 2021-12-20 22:14:58 +01:00
parent 4f5f111afe
commit 919bce65e9
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
16 changed files with 419 additions and 132 deletions

View File

@ -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
}
}

View File

@ -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(),
)
)

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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

View File

@ -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
}
/**

View File

@ -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
}
}

View File

@ -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))
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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?

View File

@ -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
)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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>