From e5189a1c7e9a38737e6d0abfd914e97a49a41f83 Mon Sep 17 00:00:00 2001 From: LagradOst Date: Fri, 2 Jul 2021 20:46:18 +0200 Subject: [PATCH] chromecast subtitle support --- .../movieproviders/HDMProvider.kt | 2 +- .../movieproviders/LookMovieProvider.kt | 2 +- .../cloudstream3/ui/ControllerActivity.kt | 110 +++++++++++++---- .../cloudstream3/ui/player/PlayerFragment.kt | 41 +++++-- .../cloudstream3/ui/result/ResultFragment.kt | 42 ++++--- .../cloudstream3/ui/result/ResultViewModel.kt | 116 ++++++++++-------- .../cloudstream3/utils/CastHelper.kt | 59 ++++++--- .../lagradost/cloudstream3/utils/DataStore.kt | 2 +- app/src/main/res/layout/sort_bottom_sheet.xml | 95 ++++++++------ app/src/main/res/values/strings.xml | 3 +- app/src/main/res/values/styles.xml | 9 ++ 11 files changed, 324 insertions(+), 157 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/HDMProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/HDMProvider.kt index b0e4b5be..5c2b56f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/HDMProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/HDMProvider.kt @@ -64,7 +64,7 @@ class HDMProvider : MainAPI() { return MovieLoadResponse( title, slug, this.name, TvType.Movie, - "$mainUrl/src/player/?v=$data", poster, year, descript, null + "$mainUrl/src/player/?v=$data", poster, year, descript, null ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/LookMovieProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/LookMovieProvider.kt index cde12366..b0b68f5d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/LookMovieProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/LookMovieProvider.kt @@ -151,7 +151,7 @@ class LookMovieProvider : MainAPI() { private fun addSubtitles(subs: List?, subtitleCallback: (SubtitleFile) -> Unit) { if (subs == null) return subs.forEach { - if (it.source != "opensubtitle") + if (it.file.endsWith(".vtt")) subtitleCallback.invoke(SubtitleFile(it.language, fixUrl(it.file))) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 6ff1c355..0e72ea68 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -1,25 +1,29 @@ package com.lagradost.cloudstream3.ui +import android.graphics.Color import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.View.* -import android.widget.AbsListView -import android.widget.ArrayAdapter -import android.widget.ImageView -import android.widget.ListView +import android.widget.* +import androidx.appcompat.app.AlertDialog +import androidx.core.graphics.toColorInt import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_OFF +import com.google.android.gms.cast.MediaTrack +import com.google.android.gms.cast.TextTrackStyle +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.media.RemoteMediaClient import com.google.android.gms.cast.framework.media.uicontroller.UIController import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity -import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.sortUrls @@ -81,6 +85,7 @@ data class MetadataHolder( val currentEpisodeIndex: Int, val episodes: List, val currentLinks: List, + val currentSubtitles: List ) class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { @@ -93,15 +98,59 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi // lateinit var dialog: AlertDialog val holder = getCurrentMetaData() + if (holder != null) { val items = holder.currentLinks if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) { - // val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) - /*val builder = BottomSheetDialog(view.context, R.style.AlertDialogCustom) - builder.setTitle("Pick source")*/ - val bottomSheetDialog = BottomSheetDialog(view.context) - bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) - val res = bottomSheetDialog.findViewById(R.id.sort_click)!! + val subTracks = + remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT } + ?: ArrayList() + + val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) + bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet) + val bottomSheetDialog = bottomSheetDialogBuilder.create() + bottomSheetDialog.show() + // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) + val providerList = bottomSheetDialog.findViewById(R.id.sort_providers)!! + val subtitleList = bottomSheetDialog.findViewById(R.id.sort_subtitles)!! + if (subTracks.isEmpty()) { + bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = GONE + } else { + val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + arrayAdapter.add("No Subtitles") + arrayAdapter.addAll(subTracks.map { it.name }.filterNotNull()) + + subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE + subtitleList.adapter = arrayAdapter + + subtitleList.setOnItemClickListener { _, _, which, _ -> + if (which == 0) { + remoteMediaClient.setActiveMediaTracks(longArrayOf()) // NO SUBS + } else { + val font = TextTrackStyle() + font.fontFamily = "Google Sans" //TODO FONT SETTINGS + font.backgroundColor = 0x00FFFFFF // TRANSPARENT + + font.edgeColor = Color.BLACK + font.edgeType = EDGE_TYPE_OUTLINE + font.foregroundColor = Color.WHITE + font.fontScale = 1.05f + + remoteMediaClient.setTextTrackStyle(font) + + remoteMediaClient.setActiveMediaTracks(longArrayOf(subTracks[which - 1].id)) + .setResultCallback { + if (!it.status.isSuccess) { + Log.e( + "CHROMECAST", "Failed with status code:" + + it.status.statusCode + " > " + it.status.statusMessage + ) + } + } + } + bottomSheetDialog.dismiss() + } + } //https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl @@ -113,12 +162,11 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) arrayAdapter.addAll(sortingMethods.toMutableList()) - res.choiceMode = AbsListView.CHOICE_MODE_SINGLE - res.adapter = arrayAdapter - res.setItemChecked(sotringIndex, true) + providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE + providerList.adapter = arrayAdapter + providerList.setItemChecked(sotringIndex, true) - - res.setOnItemClickListener { _, _, which, _ -> + providerList.setOnItemClickListener { _, _, which, _ -> val epData = holder.episodes[holder.currentEpisodeIndex] fun loadMirror(index: Int) { @@ -128,7 +176,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi epData, holder, index, - remoteMediaClient?.mediaInfo?.customData + remoteMediaClient?.mediaInfo?.customData, + holder.currentSubtitles, ) val startAt = remoteMediaClient?.approximateStreamPosition ?: 0 @@ -168,10 +217,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi bottomSheetDialog.dismiss() } - bottomSheetDialog.show() - /* - dialog = builder.create() - dialog.show()*/ } } } @@ -208,20 +253,28 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val index = meta.currentEpisodeIndex + 1 val epData = meta.episodes[index] val links = ArrayList() + val subs = ArrayList() val res = safeApiCall { - getApiFromName(meta.apiName).loadLinks(epData.data, true, { subtitleFile -> }) { - for (i in links) { - if (i.url == it.url) return@loadLinks + getApiFromName(meta.apiName).loadLinks(epData.data, true, { subtitleFile -> + if (!subs.any { it.url == subtitleFile.url }) { + subs.add(subtitleFile) + } + }) { link -> + if (!links.any { it.url == link.url }) { + links.add(link) } - links.add(it) } } if (res is Resource.Success) { val sorted = sortUrls(links) if (sorted.isNotEmpty()) { - val jsonCopy = meta.copy(currentLinks = sorted, currentEpisodeIndex = index) + val jsonCopy = meta.copy( + currentLinks = sorted, + currentSubtitles = subs, + currentEpisodeIndex = index + ) val done = withContext(Dispatchers.IO) { JSONObject(mapper.writeValueAsString(jsonCopy)) @@ -231,7 +284,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi epData, jsonCopy, 0, - done + done, + subs ) /*fun loadIndex(index: Int) { @@ -305,6 +359,8 @@ class ControllerActivity : ExpandedControllerActivity() { uiMediaController.bindViewToUIController(skipBackButton, SkipTimeController(skipBackButton, false)) uiMediaController.bindViewToUIController(skipForwardButton, SkipTimeController(skipForwardButton, true)) uiMediaController.bindViewToUIController(skipOpButton, SkipNextEpisodeController(skipOpButton)) + + /* val progressBar: CastSeekBar? = findViewById(R.id.cast_seek_bar) progressBar?.backgroundTintList = (UIHelper.adjustAlpha(colorFromAttribute(R.attr.colorPrimary), 0.35f)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt index 25d02f25..b342f9e4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt @@ -356,8 +356,10 @@ class PlayerFragment : Fragment() { } progressBarLeftHolder?.alpha = 1f - val vol = minOf(1f, - cachedVolume - diffY.toFloat() * 0.5f) // 0.05f *if (diffY > 0) 1 else -1 + val vol = minOf( + 1f, + cachedVolume - diffY.toFloat() * 0.5f + ) // 0.05f *if (diffY > 0) 1 else -1 cachedVolume = vol //progressBarRight?.progress = ((1f - alpha) * 100).toInt() @@ -405,8 +407,10 @@ class PlayerFragment : Fragment() { progressBarRight?.max = 100 * 100 progressBarRight?.progress = (alpha * 100 * 100).toInt() } else { - val alpha = minOf(0.95f, - brightness_overlay.alpha + diffY.toFloat() * 0.5f) // 0.05f *if (diffY > 0) 1 else -1 + val alpha = minOf( + 0.95f, + brightness_overlay.alpha + diffY.toFloat() * 0.5f + ) // 0.05f *if (diffY > 0) 1 else -1 brightness_overlay?.alpha = alpha progressBarRight?.max = 100 * 100 @@ -653,6 +657,7 @@ class PlayerFragment : Fragment() { private var resizeMode = 0 private var playbackSpeed = 0f private var allEpisodes: HashMap> = HashMap() + private var allEpisodesSubs: HashMap> = HashMap() private var episodes: List = ArrayList() var currentPoster: String? = null var currentHeaderName: String? = null @@ -788,8 +793,10 @@ class PlayerFragment : Fragment() { epData.index, episodes, links, + getSubs() ?: ArrayList(), index, - exoPlayer.currentPosition) + exoPlayer.currentPosition + ) /* val customData = @@ -904,6 +911,10 @@ class PlayerFragment : Fragment() { } } + observeDirectly(viewModel.allEpisodesSubs) { _allEpisodesSubs -> + allEpisodesSubs = _allEpisodesSubs + } + observeDirectly(viewModel.resultResponse) { data -> when (data) { is Resource.Success -> { @@ -971,8 +982,10 @@ class PlayerFragment : Fragment() { } overlay_loading_skip_button.setOnClickListener { - setMirrorId(sortUrls(getUrls() ?: return@setOnClickListener).first() - .getId()) // BECAUSE URLS CANT BE REORDERED + setMirrorId( + sortUrls(getUrls() ?: return@setOnClickListener).first() + .getId() + ) // BECAUSE URLS CANT BE REORDERED if (!isCurrentlyPlaying) { initPlayer(getCurrentUrl()) } @@ -1085,8 +1098,10 @@ class PlayerFragment : Fragment() { builder.setOnDismissListener { activity?.hideSystemUI() } - builder.setSingleChoiceItems(sourcesText.toTypedArray(), - sources.indexOf(getCurrentUrl())) { _, which -> + builder.setSingleChoiceItems( + sourcesText.toTypedArray(), + sources.indexOf(getCurrentUrl()) + ) { _, which -> //val speed = speedsText[which] //Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show() playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0 @@ -1156,6 +1171,14 @@ class PlayerFragment : Fragment() { } } + private fun getSubs(): List? { + return try { + allEpisodesSubs[getEpisode()?.id] + } catch (e: Exception) { + null + } + } + private fun getEpisode(): ResultEpisode? { return try { episodes[playerData.episodeIndex] diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 971ab207..e1cde11c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -164,7 +164,7 @@ class ResultFragment : Fragment() { } /// 0 = LOADING, 1 = ERROR LOADING, 2 = LOADED - fun updateVisStatus(state: Int) { + private fun updateVisStatus(state: Int) { when (state) { 0 -> { result_loading.visibility = VISIBLE @@ -207,10 +207,12 @@ class ResultFragment : Fragment() { activity?.fixPaddingStatusbar(result_barstatus) val backParameter = result_back.layoutParams as CoordinatorLayout.LayoutParams - backParameter.setMargins(backParameter.leftMargin, + backParameter.setMargins( + backParameter.leftMargin, backParameter.topMargin + requireContext().getStatusBarHeight(), backParameter.rightMargin, - backParameter.bottomMargin) + backParameter.bottomMargin + ) result_back.layoutParams = backParameter if (activity?.isCastApiAvailable() == true) { @@ -299,8 +301,9 @@ class ResultFragment : Fragment() { currentPoster, episodeClick.data.index, eps, - sortUrls(data.value), - startTime = episodeClick.data.getRealPosition() + sortUrls(data.value.links), + data.value.subs, + startTime = episodeClick.data.getRealPosition(), ) } } @@ -310,13 +313,18 @@ class ResultFragment : Fragment() { ACTION_PLAY_EPISODE_IN_PLAYER -> { if (buildInPlayer) { (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.enter_anim, + .setCustomAnimations( + R.anim.enter_anim, R.anim.exit_anim, R.anim.pop_enter, - R.anim.pop_exit) - .add(R.id.homeRoot, - PlayerFragment.newInstance(PlayerData(index, null, 0), - episodeClick.data.getRealPosition()) + R.anim.pop_exit + ) + .add( + R.id.homeRoot, + PlayerFragment.newInstance( + PlayerData(index, null, 0), + episodeClick.data.getRealPosition() + ) ) .commit() } @@ -333,10 +341,12 @@ class ResultFragment : Fragment() { if (tempUrl != null) { viewModel.loadEpisode(episodeClick.data, true) { data -> if (data is Resource.Success) { - VideoDownloadManager.DownloadEpisode(requireContext(), + VideoDownloadManager.DownloadEpisode( + requireContext(), tempUrl, episodeClick.data, - data.value) + data.value.links + ) } } } @@ -447,8 +457,12 @@ class ResultFragment : Fragment() { } if (d.year != null) metadataInfoArray.add(Pair("Year", d.year.toString())) val rating = d.rating - if (rating != null) metadataInfoArray.add(Pair("Rating", - "%.1f/10.0".format(rating.toFloat() / 10f).replace(",", "."))) + if (rating != null) metadataInfoArray.add( + Pair( + "Rating", + "%.1f/10.0".format(rating.toFloat() / 10f).replace(",", ".") + ) + ) val duration = d.duration if (duration != null) metadataInfoArray.add(Pair("Duration", duration)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index ec98cce5..e09ce310 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -70,9 +70,11 @@ class ResultViewModel : ViewModel() { private fun updateEpisodes(context: Context, localId: Int?, list: List, selection: Int?) { _episodes.postValue(list) - filterEpisodes(context, + filterEpisodes( + context, list, - if (selection == -1) context.getResultSeason(localId ?: id.value ?: return) else selection) + if (selection == -1) context.getResultSeason(localId ?: id.value ?: return) else selection + ) } fun reloadEpisodes(context: Context) { @@ -119,18 +121,20 @@ class ResultViewModel : ViewModel() { if (dataList != null) { val episodes = ArrayList() for ((index, i) in dataList.withIndex()) { - episodes.add(context.buildResultEpisode( - i.name, - i.posterUrl, - index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE - null, // TODO FIX SEASON - i.url, - apiName, - (mainId + index + 1), - index, - i.rating, - i.descript, - )) + episodes.add( + context.buildResultEpisode( + i.name, + i.posterUrl, + index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE + null, // TODO FIX SEASON + i.url, + apiName, + (mainId + index + 1), + index, + i.rating, + i.descript, + ) + ) } updateEpisodes(context, mainId, episodes, -1) } @@ -139,34 +143,40 @@ class ResultViewModel : ViewModel() { is TvSeriesLoadResponse -> { val episodes = ArrayList() for ((index, i) in d.episodes.withIndex()) { - episodes.add(context.buildResultEpisode( - i.name, - //?: (if (i.season != null && i.episode != null) "S${i.season}:E${i.episode}" else null)), // TODO ADD NAMES - i.posterUrl, - i.episode ?: (index + 1), - i.season, - i.data, - apiName, - (mainId + index + 1).hashCode(), - index, - i.rating, - i.descript - )) + episodes.add( + context.buildResultEpisode( + i.name, + //?: (if (i.season != null && i.episode != null) "S${i.season}:E${i.episode}" else null)), // TODO ADD NAMES + i.posterUrl, + i.episode ?: (index + 1), + i.season, + i.data, + apiName, + (mainId + index + 1).hashCode(), + index, + i.rating, + i.descript + ) + ) } updateEpisodes(context, mainId, episodes, -1) } is MovieLoadResponse -> { - updateEpisodes(context, mainId, arrayListOf(context.buildResultEpisode( - null, - null, - 0, null, - d.dataUrl, - d.apiName, - (mainId + 1), - 0, - null, - null, - )), -1) + updateEpisodes( + context, mainId, arrayListOf( + context.buildResultEpisode( + null, + null, + 0, null, + d.dataUrl, + d.apiName, + (mainId + 1), + 0, + null, + null, + ) + ), -1 + ) } } } @@ -179,17 +189,21 @@ class ResultViewModel : ViewModel() { private val _allEpisodes: MutableLiveData>> = MutableLiveData(HashMap()) // LOOKUP BY ID + private val _allEpisodesSubs: MutableLiveData>> = + MutableLiveData(HashMap()) // LOOKUP BY ID val allEpisodes: LiveData>> get() = _allEpisodes + val allEpisodesSubs: LiveData>> get() = _allEpisodesSubs private var _apiName: MutableLiveData = MutableLiveData() val apiName: LiveData get() = _apiName + data class EpisodeData(val links: ArrayList, val subs: ArrayList) fun loadEpisode( episode: ResultEpisode, isCasting: Boolean, - callback: (Resource>) -> Unit, + callback: (Resource) -> Unit, ) { loadEpisode(episode.id, episode.data, isCasting, callback) } @@ -198,25 +212,29 @@ class ResultViewModel : ViewModel() { id: Int, data: String, isCasting: Boolean, - callback: (Resource>) -> Unit, + callback: (Resource) -> Unit, ) = viewModelScope.launch { if (_allEpisodes.value?.contains(id) == true) { _allEpisodes.value?.remove(id) } val links = ArrayList() + val subs = ArrayList() val localData = safeApiCall { - getApiFromName(_apiName.value).loadLinks(data, isCasting, { subtitleFile -> }) { - for (i in links) { - if (i.url == it.url) return@loadLinks + getApiFromName(_apiName.value).loadLinks(data, isCasting, { subtitleFile -> + if (!subs.any { it.url == subtitleFile.url }) { + subs.add(subtitleFile) + _allEpisodesSubs.value?.set(id, subs) + _allEpisodesSubs.postValue(_allEpisodesSubs.value) + } + }) { link -> + if (!links.any { it.url == link.url }) { + links.add(link) + _allEpisodes.value?.set(id, links) + _allEpisodes.postValue(_allEpisodes.value) } - - links.add(it) - _allEpisodes.value?.set(id, links) - _allEpisodes.postValue(_allEpisodes.value) - // _allEpisodes.value?.get(episode.id)?.add(it) } - links + EpisodeData(links, subs) } callback.invoke(localData) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt index 29eb4fcd..634d8b5c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt @@ -7,15 +7,13 @@ import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.google.android.exoplayer2.ext.cast.CastPlayer import com.google.android.exoplayer2.util.MimeTypes -import com.google.android.gms.cast.CastStatusCodes -import com.google.android.gms.cast.MediaInfo -import com.google.android.gms.cast.MediaMetadata -import com.google.android.gms.cast.MediaQueueItem +import com.google.android.gms.cast.* import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_OFF import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.media.RemoteMediaClient import com.google.android.gms.common.api.PendingResult import com.google.android.gms.common.images.WebImage +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.MetadataHolder import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.main @@ -27,14 +25,22 @@ object CastHelper { private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() - fun getMediaInfo(epData: ResultEpisode, holder: MetadataHolder, index: Int, data: JSONObject?): MediaInfo { + fun getMediaInfo( + epData: ResultEpisode, + holder: MetadataHolder, + index: Int, + data: JSONObject?, + subtitles: List + ): MediaInfo { val link = holder.currentLinks[index] val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) - movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, + movieMetadata.putString( + MediaMetadata.KEY_SUBTITLE, if (holder.isMovie) link.name else - (epData.name ?: "Episode ${epData.episode}") + " - ${link.name}") + (epData.name ?: "Episode ${epData.episode}") + " - ${link.name}" + ) movieMetadata.putString(MediaMetadata.KEY_TITLE, holder.title) @@ -43,11 +49,21 @@ object CastHelper { movieMetadata.addImage(WebImage(Uri.parse(srcPoster))) } + var subIndex = 0 + val tracks = subtitles.map { + MediaTrack.Builder(subIndex++.toLong(), MediaTrack.TYPE_TEXT) + .setName(it.lang) + .setSubtype(MediaTrack.SUBTYPE_SUBTITLES) + .setContentId(it.url) + .build() + } + return MediaInfo.Builder(link.url) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(MimeTypes.VIDEO_UNKNOWN) .setCustomData(data) .setMetadata(movieMetadata) + .setMediaTracks(tracks) .build() } @@ -76,39 +92,48 @@ object CastHelper { currentEpisodeIndex: Int, episodes: List, currentLinks: List, + subtitles: List, startIndex: Int? = null, startTime: Long? = null, - ) { - if (episodes.isEmpty()) return + ) : Boolean { + if (episodes.isEmpty()) return false + if (currentLinks.size <= currentEpisodeIndex) return false val castContext = CastContext.getSharedInstance(this) val epData = episodes[currentEpisodeIndex] - val holder = MetadataHolder(apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks) + val holder = + MetadataHolder(apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks, subtitles) val index = startIndex ?: 0 val mediaItem = - getMediaInfo(epData, holder, index, JSONObject(mapper.writeValueAsString(holder))) + getMediaInfo(epData, holder, index, JSONObject(mapper.writeValueAsString(holder)), subtitles) val castPlayer = CastPlayer(castContext) castPlayer.repeatMode = REPEAT_MODE_REPEAT_OFF - awaitLinks(castPlayer.loadItem( - MediaQueueItem.Builder(mediaItem).build(), - startTime ?: 0, - )) { + awaitLinks( + castPlayer.loadItem( + MediaQueueItem.Builder(mediaItem).build(), + startTime ?: 0, + ) + ) { if (currentLinks.size > index + 1) - startCast(apiName, + startCast( + apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks, + subtitles, index + 1, - startTime) + startTime + ) } + return true } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index f8f2f50c..ca9a4d60 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -91,6 +91,6 @@ object DataStore { } inline fun Context.getKey(folder: String, path: String, defVal: T?): T? { - return getKey(getFolderName(folder, path), defVal) + return getKey(getFolderName(folder, path), defVal) ?: defVal } } \ No newline at end of file diff --git a/app/src/main/res/layout/sort_bottom_sheet.xml b/app/src/main/res/layout/sort_bottom_sheet.xml index a79b6366..d3c004ad 100644 --- a/app/src/main/res/layout/sort_bottom_sheet.xml +++ b/app/src/main/res/layout/sort_bottom_sheet.xml @@ -1,48 +1,69 @@ - - + + - - - + android:layout_height="0dp" + android:orientation="vertical" + android:layout_weight="50"> + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4d7a345..a8d0f6eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,8 @@ Plan to Watch None Play Movie - Pick Source + Sources + Subtitles Retry connection… Go Back Episode Poster diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 443bae90..c23bbad9 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -118,6 +118,15 @@ +