fix playback & update to agp 7.4.0

updated the crunchyroll parser to use the new streams endpoint to retrieve the media streams
This commit is contained in:
Jannik 2023-01-25 19:51:38 +01:00
parent 9380f98098
commit 097383a082
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
12 changed files with 87 additions and 91 deletions

View File

@ -12,8 +12,8 @@ android {
applicationId "org.mosad.teapod"
minSdkVersion 23
targetSdkVersion 32
versionCode 100000 //01.00.000
versionName "1.0.0"
versionCode 100990 //01.00.000
versionName "1.1.0-beta1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
@ -53,7 +53,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
@ -80,8 +80,8 @@ dependencies {
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

View File

@ -52,6 +52,9 @@
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
# This is generated automatically by the Android Gradle plugin.
-dontwarn org.slf4j.impl.StaticLoggerBinder
#misc
-dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn java.lang.ClassValue

View File

@ -305,7 +305,8 @@ object Crunchyroll {
// if the cache has more than 100 entries clear it, so it doesn't become a memory problem
// Note: this value is totally guessed and should be replaced by a properly researched value
if (browsingCache.size > 100) {
// TODO 100 is way to high as it's not the number of items but BrowseResults
if (browsingCache.size > 10) {
browsingCache.clear()
}
@ -436,13 +437,10 @@ object Crunchyroll {
* @return A **[Seasons]** object with a list of **[Season]**
*/
suspend fun seasons(seriesId: String): Seasons {
val seasonsEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/seasons"
val seasonsEndpoint = "/content/v2/cms/series/$seriesId/seasons"
val parameters = listOf(
"series_id" to seriesId,
"locale" to Preferences.preferredLocale.toLanguageTag(),
"Signature" to signature,
"Policy" to policy,
"Key-Pair-Id" to keyPairID
"preferred_audio_language" to Preferences.preferredLocale.toLanguageTag(),
"locale" to Preferences.preferredLocale.toLanguageTag()
)
return try {
@ -460,13 +458,10 @@ object Crunchyroll {
* @return A **[Episodes]** object with a list of **[Episode]**
*/
suspend fun episodes(seasonId: String): Episodes {
val episodesEndpoint = "/cms/v2/${token.country}/M3/crunchyroll/episodes"
val episodesEndpoint = "/content/v2/cms/seasons/$seasonId/episodes"
val parameters = listOf(
"season_id" to seasonId,
"locale" to Preferences.preferredLocale.toLanguageTag(),
"Signature" to signature,
"Policy" to policy,
"Key-Pair-Id" to keyPairID
"preferred_audio_language" to Preferences.preferredLocale.toLanguageTag(),
"locale" to Preferences.preferredLocale.toLanguageTag()
)
return try {
@ -480,15 +475,20 @@ object Crunchyroll {
/**
* Get all available subtitles and streams of a episode.
*
* @param url The playback url of a episode
* @return A **[Playback]** object
* @param url The streams url of a episode
* @return A **[Streams]** object
*/
suspend fun playback(url: String): Playback {
suspend fun streams(url: String): Streams {
val parameters = listOf(
"preferred_audio_language" to Preferences.preferredLocale.toLanguageTag(),
"locale" to Preferences.preferredLocale.toLanguageTag()
)
return try {
requestGet("", url = url)
requestGet(url, parameters)
} catch (ex: SerializationException) {
Log.e(TAG, "SerializationException in playback(), with url = $url.", ex)
NonePlayback
Log.e(TAG, "SerializationException in streams().", ex)
NoneStreams
}
}

View File

@ -255,16 +255,16 @@ val NoneSeries = Series("", "", "", Images(emptyList(), emptyList()), emptyList(
@Serializable
data class Seasons(
@SerialName("total") val total: Int,
@SerialName("items") val items: List<Season>
@SerialName("data") val data: List<Season>
) {
fun getPreferredSeasonByLocal(local: Locale): Season {
return items.firstOrNull { season ->
return data.firstOrNull { season ->
// try to get the the first seasons which matches the preferred local
season.slugTitle.endsWith("${local.getDisplayLanguage(Locale.ENGLISH)}-dub", true)
} ?: items.firstOrNull { season ->
} ?: data.firstOrNull { season ->
// if there is no season with the preferred local, try to find a subbed season
season.isSubbed
} ?: items.first() // if no preferred language and no sub, use the first season
} ?: data.first() // if no preferred language and no sub, use the first season
}
}
@ -289,7 +289,7 @@ val NoneSeason = Season("", "", "", "", 0, isSubbed = false, isDubbed = false)
@Serializable
data class Episodes(
@SerialName("total") val total: Int,
@SerialName("items") val items: List<Episode>
@SerialName("data") val data: List<Episode>
)
@Serializable
@ -310,6 +310,7 @@ data class Episode(
@SerialName("images") val images: Thumbnail,
@SerialName("duration_ms") val durationMs: Int,
@SerialName("playback") val playback: String,
@SerialName("streams_link") val streamsLink: String,
)
@Serializable
@ -334,7 +335,8 @@ val NoneEpisode = Episode(
isDubbed = false,
images = Thumbnail(listOf()),
durationMs = 0,
playback = ""
playback = "",
streamsLink = ""
)
typealias PlayheadsMap = Map<String, PlayheadObject>
@ -366,34 +368,26 @@ val NoneDatalabIntro = DatalabIntro("", 0f, 0f, 0f, "", "", "")
/**
* playback/stream data classes
*/
@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("total") val total: Int,
@SerialName("data") val data: List<StreamList>,
)
@Serializable
data class StreamList(
@SerialName("adaptive_dash") val adaptive_dash: Map<String, Stream>,
@SerialName("adaptive_hls") val adaptive_hls: Map<String, Stream>,
@SerialName("download_dash") val downloadDash: 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>,
// @SerialName("drm_adaptive_dash") val drmAdaptiveDash: Map<String, Stream>,
// @SerialName("drm_adaptive_hls") val drmAdaptiveHls: Map<String, Stream>,
// @SerialName("drm_download_dash") val drmDownloadDash: Map<String, Stream>,
// @SerialName("drm_download_hls") val drmDownloadHls: 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
@ -403,13 +397,11 @@ data class Stream(
@SerialName("vcodec") val vcodec: String = "", // default/nullable value since optional
)
val NonePlayback = Playback(
"",
mapOf(),
Streams(
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
mapOf(), mapOf(), mapOf(), mapOf(), mapOf(), mapOf(),
)
val NoneStreams = Streams(
0,
arrayListOf(StreamList(
mapOf(), mapOf(), mapOf(), mapOf()
))
)
/**

View File

@ -98,7 +98,7 @@ class MediaFragment(private val mediaIdStr: String) : Fragment() {
.into(binding.imageBackdrop)
binding.textYear.text = when(tmdbResult) {
is TMDBTVShow -> (tmdbResult as TMDBTVShow).firstAirDate.substring(0, 4)
is TMDBTVShow -> (tmdbResult as TMDBTVShow).firstAirDate?.substring(0, 4)
is TMDBMovie -> (tmdbResult as TMDBMovie).releaseDate.substring(0, 4)
else -> ""
}

View File

@ -67,7 +67,7 @@ class MediaFragmentEpisodes : Fragment() {
private fun showSeasonSelection(v: View) {
// TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus
val popup = PopupMenu(requireContext(), v)
model.seasonsCrunchy.items.forEach { season ->
model.seasonsCrunchy.data.forEach { season ->
popup.menu.add(getString(
R.string.season_number_title,
season.seasonNumber,

View File

@ -59,7 +59,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
// load the preferred season:
// next episode > preferred language (language per season, not per stream)
currentSeasonCrunchy = seasonsCrunchy.items.firstOrNull{ season ->
currentSeasonCrunchy = seasonsCrunchy.data.firstOrNull{ season ->
season.id == upNextSeries.panel.episodeMetadata.seasonId
} ?: seasonsCrunchy.getPreferredSeasonByLocal(Preferences.preferredLocale)
// Note: if we need to query metaDB, do it now
@ -67,10 +67,10 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
// load episodes and metaDB in parallel (tmdb needs mediaType, which is set via episodes)
viewModelScope.launch { episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) }.join()
currentEpisodesCrunchy.clear()
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
currentEpisodesCrunchy.addAll(episodesCrunchy.data)
// set media type
mediaType = episodesCrunchy.items.firstOrNull()?.let {
mediaType = episodesCrunchy.data.firstOrNull()?.let {
if (it.episodeNumber != null) MediaType.TVSHOW else MediaType.MOVIE
} ?: MediaType.OTHER
@ -78,7 +78,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
listOf(
viewModelScope.launch {
// get playheads (including fully watched state)
val episodeIDs = episodesCrunchy.items.map { it.id }
val episodeIDs = episodesCrunchy.data.map { it.id }
currentPlayheads.clear()
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
},
@ -124,16 +124,16 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
// set currentSeasonCrunchy to the new season with id == seasonId, if the id isn't found,
// don't change the current season (this should/can never happen)
currentSeasonCrunchy = seasonsCrunchy.items.firstOrNull {
currentSeasonCrunchy = seasonsCrunchy.data.firstOrNull {
it.id == seasonId
} ?: currentSeasonCrunchy
episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
currentEpisodesCrunchy.clear()
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
currentEpisodesCrunchy.addAll(episodesCrunchy.data)
// update playheads playheads (including fully watched state)
val episodeIDs = episodesCrunchy.items.map { it.id }
val episodeIDs = episodesCrunchy.data.map { it.id }
currentPlayheads.clear()
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
}
@ -151,7 +151,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
suspend fun updateOnResume() {
joinAll(
viewModelScope.launch {
val episodeIDs = episodesCrunchy.items.map { it.id }
val episodeIDs = episodesCrunchy.data.map { it.id }
currentPlayheads.clear()
currentPlayheads.putAll(Crunchyroll.playheads(episodeIDs))
},

View File

@ -75,7 +75,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
internal set
var currentEpisode = NoneEpisode
internal set
var currentPlayback = NonePlayback
var currentStreams = NoneStreams
// current playback settings
var currentLanguage: Locale = Preferences.preferredLocale
@ -134,9 +134,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
episodes = Crunchyroll.episodes(seasonId)
listOf(
viewModelScope.launch { mediaMeta = loadMediaMeta(episodes.items.first().seriesId) },
viewModelScope.launch { mediaMeta = loadMediaMeta(episodes.data.first().seriesId) },
viewModelScope.launch {
val episodeIDs = episodes.items.map { it.id }
val episodeIDs = episodes.data.map { it.id }
currentPlayheads = Crunchyroll.playheads(episodeIDs)
}
).joinAll()
@ -178,7 +178,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
* @param episodeId The ID of the episode you want to set currentEpisodeCr to
*/
suspend fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) {
currentEpisode = episodes.items.find { episode ->
currentEpisode = episodes.data.find { episode ->
episode.id == episodeId
} ?: NoneEpisode
@ -198,7 +198,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
// needs to be blocking, currentPlayback must be present when calling playCurrentMedia()
joinAll(
viewModelScope.launch(Dispatchers.IO) {
currentPlayback = Crunchyroll.playback(currentEpisode.playback)
currentStreams = Crunchyroll.streams(currentEpisode.streamsLink)
println("stream: $Streams")
},
viewModelScope.launch(Dispatchers.IO) {
Crunchyroll.playheads(listOf(currentEpisode.id))[currentEpisode.id]?.let {
@ -211,10 +212,10 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
}
},
viewModelScope.launch(Dispatchers.IO) {
currentIntroMetadata = Crunchyroll.datalabIntro(currentEpisode.id)
currentIntroMetadata = NoneDatalabIntro //Crunchyroll.datalabIntro(currentEpisode.id)
}
)
Log.d(classTag, "playback: ${currentEpisode.playback}")
Log.d(classTag, "playback: ${currentEpisode.streamsLink}")
if (startPlayback) {
playCurrentMedia()
@ -231,17 +232,17 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
val preferredLocale = currentLanguage
val fallbackLocal = Locale.US
val url = when {
currentPlayback.streams.adaptive_hls.containsKey(preferredLocale.toLanguageTag()) -> {
currentPlayback.streams.adaptive_hls[preferredLocale.toLanguageTag()]?.url
currentStreams.data[0].adaptive_hls.containsKey(preferredLocale.toLanguageTag()) -> {
currentStreams.data[0].adaptive_hls[preferredLocale.toLanguageTag()]?.url
}
currentPlayback.streams.adaptive_hls.containsKey(fallbackLocal.toLanguageTag()) -> {
currentStreams.data[0].adaptive_hls.containsKey(fallbackLocal.toLanguageTag()) -> {
currentLanguage = fallbackLocal
currentPlayback.streams.adaptive_hls[fallbackLocal.toLanguageTag()]?.url
currentStreams.data[0].adaptive_hls[fallbackLocal.toLanguageTag()]?.url
}
else -> {
// if no language tag is present use the first entry
currentLanguage = Locale.ROOT
currentPlayback.streams.adaptive_hls.entries.first().value.url
currentStreams.data[0].adaptive_hls.entries.first().value.url
}
}
Log.i(classTag, "stream url: $url")
@ -277,7 +278,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
* @return Boolean: true if it is the last, else false.
*/
fun currentEpisodeIsLastEpisode(): Boolean {
return episodes.items.lastOrNull()?.id == currentEpisode.id
return episodes.data.lastOrNull()?.id == currentEpisode.id
}
private suspend fun loadMediaMeta(crSeriesId: String): Meta? {
@ -297,7 +298,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
}
viewModelScope.launch {
val episodeIDs = episodes.items.map { it.id }
val episodeIDs = episodes.data.map { it.id }
currentPlayheads = Crunchyroll.playheads(episodeIDs)
}
}

View File

@ -42,7 +42,7 @@ class EpisodeListDialogFragment : DialogFragment() {
}
val adapterRecEpisodes = EpisodeItemAdapter(
model.episodes.items,
model.episodes.data,
null,
model.currentPlayheads.toMap(),
EpisodeItemAdapter.OnClickListener { episode ->
@ -56,7 +56,7 @@ class EpisodeListDialogFragment : DialogFragment() {
)
// get the position/index of the currently playing episode
adapterRecEpisodes.currentSelected = model.episodes.items.indexOfFirst { it.id == model.currentEpisode.id }
adapterRecEpisodes.currentSelected = model.episodes.data.indexOfFirst { it.id == model.currentEpisode.id }
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)

View File

@ -46,7 +46,7 @@ class LanguageSettingsDialogFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.currentPlayback.streams.adaptive_hls.keys.forEach { languageTag ->
model.currentStreams.data[0].adaptive_hls.keys.forEach { languageTag ->
val locale = Locale.forLanguageTag(languageTag)
addLanguage(locale, locale == model.currentLanguage) { v ->
selectedLocale = locale

View File

@ -102,9 +102,9 @@ data class TMDBTVShow(
@SerialName("overview")override val overview: String,
@SerialName("poster_path") override val posterPath: String?,
@SerialName("backdrop_path") override val backdropPath: String?,
@SerialName("first_air_date") val firstAirDate: String,
@SerialName("last_air_date") val lastAirDate: String,
@SerialName("status") val status: String,
@SerialName("first_air_date") val firstAirDate: String?,
@SerialName("last_air_date") val lastAirDate: String?,
@SerialName("status") val status: String?,
// TODO genres
) : TMDBResult

View File

@ -8,7 +8,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.android.tools.build:gradle:7.4.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong