Implement media fragment for tv shows
parent
a46fd4c6d2
commit
236ca9a6c9
|
@ -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>
|
Loading…
Reference in New Issue