From 097383a082ed11a2c9068a228bd7998fe63d1eb5 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 25 Jan 2023 19:51:38 +0100 Subject: [PATCH] fix playback & update to agp 7.4.0 updated the crunchyroll parser to use the new streams endpoint to retrieve the media streams --- app/build.gradle | 10 +-- app/proguard-rules.pro | 3 + .../teapod/parser/crunchyroll/Crunchyroll.kt | 38 +++++------ .../teapod/parser/crunchyroll/DataTypes.kt | 64 ++++++++----------- .../activity/main/fragments/MediaFragment.kt | 2 +- .../main/fragments/MediaFragmentEpisodes.kt | 2 +- .../main/viewmodel/MediaFragmentViewModel.kt | 16 ++--- .../ui/activity/player/PlayerViewModel.kt | 29 +++++---- .../fragment/EpisodeListDialogFragment.kt | 4 +- .../LanguageSettingsDialogFragment.kt | 2 +- .../mosad/teapod/util/tmdb/TMDBDataTypes.kt | 6 +- build.gradle | 2 +- 12 files changed, 87 insertions(+), 91 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 24fec28..5ebbd88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 73bba8f..2ef15c1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 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 3dfe811..10fc05e 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 @@ -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 } } 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 230fa67..cb3435d 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 @@ -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 + @SerialName("data") val data: List ) { 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 + @SerialName("data") val data: List ) @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 @@ -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, - @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, +) + +@Serializable +data class StreamList( @SerialName("adaptive_dash") val adaptive_dash: Map, @SerialName("adaptive_hls") val adaptive_hls: Map, + @SerialName("download_dash") val downloadDash: 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, +// @SerialName("drm_adaptive_dash") val drmAdaptiveDash: Map, +// @SerialName("drm_adaptive_hls") val drmAdaptiveHls: Map, +// @SerialName("drm_download_dash") val drmDownloadDash: Map, +// @SerialName("drm_download_hls") val drmDownloadHls: 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 @@ -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() + )) ) /** 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 9237b82..1d803e3 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 @@ -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 -> "" } 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 3e3307d..f4ec379 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 @@ -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, 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 4f02fbf..d69554e 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 @@ -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)) }, 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 bad0501..584d20e 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 @@ -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) } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/EpisodeListDialogFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/EpisodeListDialogFragment.kt index a35fb9f..01d02d3 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/EpisodeListDialogFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/EpisodeListDialogFragment.kt @@ -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) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/LanguageSettingsDialogFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/LanguageSettingsDialogFragment.kt index deea63b..f3b16f1 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/LanguageSettingsDialogFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/fragment/LanguageSettingsDialogFragment.kt @@ -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 diff --git a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt index 3475cf9..05f9f77 100644 --- a/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/tmdb/TMDBDataTypes.kt @@ -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 diff --git a/build.gradle b/build.gradle index a74c50e..519d2b2 100644 --- a/build.gradle +++ b/build.gradle @@ -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