chromecast subtitle support

This commit is contained in:
LagradOst 2021-07-02 20:46:18 +02:00
parent 9fc732c68c
commit e5189a1c7e
11 changed files with 324 additions and 157 deletions

View file

@ -64,7 +64,7 @@ class HDMProvider : MainAPI() {
return MovieLoadResponse( return MovieLoadResponse(
title, slug, this.name, TvType.Movie, title, slug, this.name, TvType.Movie,
"$mainUrl/src/player/?v=$data", poster, year, descript, null "$mainUrl/src/player/?v=$data", poster, year, descript, null
) )
} }
} }

View file

@ -151,7 +151,7 @@ class LookMovieProvider : MainAPI() {
private fun addSubtitles(subs: List<LookMovieTokenSubtitle>?, subtitleCallback: (SubtitleFile) -> Unit) { private fun addSubtitles(subs: List<LookMovieTokenSubtitle>?, subtitleCallback: (SubtitleFile) -> Unit) {
if (subs == null) return if (subs == null) return
subs.forEach { subs.forEach {
if (it.source != "opensubtitle") if (it.file.endsWith(".vtt"))
subtitleCallback.invoke(SubtitleFile(it.language, fixUrl(it.file))) subtitleCallback.invoke(SubtitleFile(it.language, fixUrl(it.file)))
} }
} }

View file

@ -1,25 +1,29 @@
package com.lagradost.cloudstream3.ui package com.lagradost.cloudstream3.ui
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.View.* import android.view.View.*
import android.widget.AbsListView import android.widget.*
import android.widget.ArrayAdapter import androidx.appcompat.app.AlertDialog
import android.widget.ImageView import androidx.core.graphics.toColorInt
import android.widget.ListView
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_OFF 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.CastButtonFactory
import com.google.android.gms.cast.framework.CastSession 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.RemoteMediaClient
import com.google.android.gms.cast.framework.media.uicontroller.UIController import com.google.android.gms.cast.framework.media.uicontroller.UIController
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity 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.APIHolder.getApiFromName
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.sortUrls import com.lagradost.cloudstream3.sortUrls
@ -81,6 +85,7 @@ data class MetadataHolder(
val currentEpisodeIndex: Int, val currentEpisodeIndex: Int,
val episodes: List<ResultEpisode>, val episodes: List<ResultEpisode>,
val currentLinks: List<ExtractorLink>, val currentLinks: List<ExtractorLink>,
val currentSubtitles: List<SubtitleFile>
) )
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { 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 // lateinit var dialog: AlertDialog
val holder = getCurrentMetaData() val holder = getCurrentMetaData()
if (holder != null) { if (holder != null) {
val items = holder.currentLinks val items = holder.currentLinks
if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) { if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) {
// val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) val subTracks =
/*val builder = BottomSheetDialog(view.context, R.style.AlertDialogCustom) remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT }
builder.setTitle("Pick source")*/ ?: ArrayList()
val bottomSheetDialog = BottomSheetDialog(view.context)
bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
val res = bottomSheetDialog.findViewById<ListView>(R.id.sort_click)!! bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet)
val bottomSheetDialog = bottomSheetDialogBuilder.create()
bottomSheetDialog.show()
// bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
val providerList = bottomSheetDialog.findViewById<ListView>(R.id.sort_providers)!!
val subtitleList = bottomSheetDialog.findViewById<ListView>(R.id.sort_subtitles)!!
if (subTracks.isEmpty()) {
bottomSheetDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
} else {
val arrayAdapter = ArrayAdapter<String>(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 //https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation
val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl
@ -113,12 +162,11 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val arrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice) val arrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
arrayAdapter.addAll(sortingMethods.toMutableList()) arrayAdapter.addAll(sortingMethods.toMutableList())
res.choiceMode = AbsListView.CHOICE_MODE_SINGLE providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
res.adapter = arrayAdapter providerList.adapter = arrayAdapter
res.setItemChecked(sotringIndex, true) providerList.setItemChecked(sotringIndex, true)
providerList.setOnItemClickListener { _, _, which, _ ->
res.setOnItemClickListener { _, _, which, _ ->
val epData = holder.episodes[holder.currentEpisodeIndex] val epData = holder.episodes[holder.currentEpisodeIndex]
fun loadMirror(index: Int) { fun loadMirror(index: Int) {
@ -128,7 +176,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
epData, epData,
holder, holder,
index, index,
remoteMediaClient?.mediaInfo?.customData remoteMediaClient?.mediaInfo?.customData,
holder.currentSubtitles,
) )
val startAt = remoteMediaClient?.approximateStreamPosition ?: 0 val startAt = remoteMediaClient?.approximateStreamPosition ?: 0
@ -168,10 +217,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
bottomSheetDialog.dismiss() 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 index = meta.currentEpisodeIndex + 1
val epData = meta.episodes[index] val epData = meta.episodes[index]
val links = ArrayList<ExtractorLink>() val links = ArrayList<ExtractorLink>()
val subs = ArrayList<SubtitleFile>()
val res = safeApiCall { val res = safeApiCall {
getApiFromName(meta.apiName).loadLinks(epData.data, true, { subtitleFile -> }) { getApiFromName(meta.apiName).loadLinks(epData.data, true, { subtitleFile ->
for (i in links) { if (!subs.any { it.url == subtitleFile.url }) {
if (i.url == it.url) return@loadLinks subs.add(subtitleFile)
}
}) { link ->
if (!links.any { it.url == link.url }) {
links.add(link)
} }
links.add(it)
} }
} }
if (res is Resource.Success) { if (res is Resource.Success) {
val sorted = sortUrls(links) val sorted = sortUrls(links)
if (sorted.isNotEmpty()) { 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) { val done = withContext(Dispatchers.IO) {
JSONObject(mapper.writeValueAsString(jsonCopy)) JSONObject(mapper.writeValueAsString(jsonCopy))
@ -231,7 +284,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
epData, epData,
jsonCopy, jsonCopy,
0, 0,
done done,
subs
) )
/*fun loadIndex(index: Int) { /*fun loadIndex(index: Int) {
@ -305,6 +359,8 @@ class ControllerActivity : ExpandedControllerActivity() {
uiMediaController.bindViewToUIController(skipBackButton, SkipTimeController(skipBackButton, false)) uiMediaController.bindViewToUIController(skipBackButton, SkipTimeController(skipBackButton, false))
uiMediaController.bindViewToUIController(skipForwardButton, SkipTimeController(skipForwardButton, true)) uiMediaController.bindViewToUIController(skipForwardButton, SkipTimeController(skipForwardButton, true))
uiMediaController.bindViewToUIController(skipOpButton, SkipNextEpisodeController(skipOpButton)) uiMediaController.bindViewToUIController(skipOpButton, SkipNextEpisodeController(skipOpButton))
/* val progressBar: CastSeekBar? = findViewById(R.id.cast_seek_bar) /* val progressBar: CastSeekBar? = findViewById(R.id.cast_seek_bar)
progressBar?.backgroundTintList = (UIHelper.adjustAlpha(colorFromAttribute(R.attr.colorPrimary), 0.35f)) progressBar?.backgroundTintList = (UIHelper.adjustAlpha(colorFromAttribute(R.attr.colorPrimary), 0.35f))

View file

@ -356,8 +356,10 @@ class PlayerFragment : Fragment() {
} }
progressBarLeftHolder?.alpha = 1f progressBarLeftHolder?.alpha = 1f
val vol = minOf(1f, val vol = minOf(
cachedVolume - diffY.toFloat() * 0.5f) // 0.05f *if (diffY > 0) 1 else -1 1f,
cachedVolume - diffY.toFloat() * 0.5f
) // 0.05f *if (diffY > 0) 1 else -1
cachedVolume = vol cachedVolume = vol
//progressBarRight?.progress = ((1f - alpha) * 100).toInt() //progressBarRight?.progress = ((1f - alpha) * 100).toInt()
@ -405,8 +407,10 @@ class PlayerFragment : Fragment() {
progressBarRight?.max = 100 * 100 progressBarRight?.max = 100 * 100
progressBarRight?.progress = (alpha * 100 * 100).toInt() progressBarRight?.progress = (alpha * 100 * 100).toInt()
} else { } else {
val alpha = minOf(0.95f, val alpha = minOf(
brightness_overlay.alpha + diffY.toFloat() * 0.5f) // 0.05f *if (diffY > 0) 1 else -1 0.95f,
brightness_overlay.alpha + diffY.toFloat() * 0.5f
) // 0.05f *if (diffY > 0) 1 else -1
brightness_overlay?.alpha = alpha brightness_overlay?.alpha = alpha
progressBarRight?.max = 100 * 100 progressBarRight?.max = 100 * 100
@ -653,6 +657,7 @@ class PlayerFragment : Fragment() {
private var resizeMode = 0 private var resizeMode = 0
private var playbackSpeed = 0f private var playbackSpeed = 0f
private var allEpisodes: HashMap<Int, ArrayList<ExtractorLink>> = HashMap() private var allEpisodes: HashMap<Int, ArrayList<ExtractorLink>> = HashMap()
private var allEpisodesSubs: HashMap<Int, ArrayList<SubtitleFile>> = HashMap()
private var episodes: List<ResultEpisode> = ArrayList() private var episodes: List<ResultEpisode> = ArrayList()
var currentPoster: String? = null var currentPoster: String? = null
var currentHeaderName: String? = null var currentHeaderName: String? = null
@ -788,8 +793,10 @@ class PlayerFragment : Fragment() {
epData.index, epData.index,
episodes, episodes,
links, links,
getSubs() ?: ArrayList(),
index, index,
exoPlayer.currentPosition) exoPlayer.currentPosition
)
/* /*
val customData = val customData =
@ -904,6 +911,10 @@ class PlayerFragment : Fragment() {
} }
} }
observeDirectly(viewModel.allEpisodesSubs) { _allEpisodesSubs ->
allEpisodesSubs = _allEpisodesSubs
}
observeDirectly(viewModel.resultResponse) { data -> observeDirectly(viewModel.resultResponse) { data ->
when (data) { when (data) {
is Resource.Success -> { is Resource.Success -> {
@ -971,8 +982,10 @@ class PlayerFragment : Fragment() {
} }
overlay_loading_skip_button.setOnClickListener { overlay_loading_skip_button.setOnClickListener {
setMirrorId(sortUrls(getUrls() ?: return@setOnClickListener).first() setMirrorId(
.getId()) // BECAUSE URLS CANT BE REORDERED sortUrls(getUrls() ?: return@setOnClickListener).first()
.getId()
) // BECAUSE URLS CANT BE REORDERED
if (!isCurrentlyPlaying) { if (!isCurrentlyPlaying) {
initPlayer(getCurrentUrl()) initPlayer(getCurrentUrl())
} }
@ -1085,8 +1098,10 @@ class PlayerFragment : Fragment() {
builder.setOnDismissListener { builder.setOnDismissListener {
activity?.hideSystemUI() activity?.hideSystemUI()
} }
builder.setSingleChoiceItems(sourcesText.toTypedArray(), builder.setSingleChoiceItems(
sources.indexOf(getCurrentUrl())) { _, which -> sourcesText.toTypedArray(),
sources.indexOf(getCurrentUrl())
) { _, which ->
//val speed = speedsText[which] //val speed = speedsText[which]
//Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show() //Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show()
playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0 playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0
@ -1156,6 +1171,14 @@ class PlayerFragment : Fragment() {
} }
} }
private fun getSubs(): List<SubtitleFile>? {
return try {
allEpisodesSubs[getEpisode()?.id]
} catch (e: Exception) {
null
}
}
private fun getEpisode(): ResultEpisode? { private fun getEpisode(): ResultEpisode? {
return try { return try {
episodes[playerData.episodeIndex] episodes[playerData.episodeIndex]

View file

@ -164,7 +164,7 @@ class ResultFragment : Fragment() {
} }
/// 0 = LOADING, 1 = ERROR LOADING, 2 = LOADED /// 0 = LOADING, 1 = ERROR LOADING, 2 = LOADED
fun updateVisStatus(state: Int) { private fun updateVisStatus(state: Int) {
when (state) { when (state) {
0 -> { 0 -> {
result_loading.visibility = VISIBLE result_loading.visibility = VISIBLE
@ -207,10 +207,12 @@ class ResultFragment : Fragment() {
activity?.fixPaddingStatusbar(result_barstatus) activity?.fixPaddingStatusbar(result_barstatus)
val backParameter = result_back.layoutParams as CoordinatorLayout.LayoutParams val backParameter = result_back.layoutParams as CoordinatorLayout.LayoutParams
backParameter.setMargins(backParameter.leftMargin, backParameter.setMargins(
backParameter.leftMargin,
backParameter.topMargin + requireContext().getStatusBarHeight(), backParameter.topMargin + requireContext().getStatusBarHeight(),
backParameter.rightMargin, backParameter.rightMargin,
backParameter.bottomMargin) backParameter.bottomMargin
)
result_back.layoutParams = backParameter result_back.layoutParams = backParameter
if (activity?.isCastApiAvailable() == true) { if (activity?.isCastApiAvailable() == true) {
@ -299,8 +301,9 @@ class ResultFragment : Fragment() {
currentPoster, currentPoster,
episodeClick.data.index, episodeClick.data.index,
eps, eps,
sortUrls(data.value), sortUrls(data.value.links),
startTime = episodeClick.data.getRealPosition() data.value.subs,
startTime = episodeClick.data.getRealPosition(),
) )
} }
} }
@ -310,13 +313,18 @@ class ResultFragment : Fragment() {
ACTION_PLAY_EPISODE_IN_PLAYER -> { ACTION_PLAY_EPISODE_IN_PLAYER -> {
if (buildInPlayer) { if (buildInPlayer) {
(requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_anim, .setCustomAnimations(
R.anim.enter_anim,
R.anim.exit_anim, R.anim.exit_anim,
R.anim.pop_enter, R.anim.pop_enter,
R.anim.pop_exit) R.anim.pop_exit
.add(R.id.homeRoot, )
PlayerFragment.newInstance(PlayerData(index, null, 0), .add(
episodeClick.data.getRealPosition()) R.id.homeRoot,
PlayerFragment.newInstance(
PlayerData(index, null, 0),
episodeClick.data.getRealPosition()
)
) )
.commit() .commit()
} }
@ -333,10 +341,12 @@ class ResultFragment : Fragment() {
if (tempUrl != null) { if (tempUrl != null) {
viewModel.loadEpisode(episodeClick.data, true) { data -> viewModel.loadEpisode(episodeClick.data, true) { data ->
if (data is Resource.Success) { if (data is Resource.Success) {
VideoDownloadManager.DownloadEpisode(requireContext(), VideoDownloadManager.DownloadEpisode(
requireContext(),
tempUrl, tempUrl,
episodeClick.data, 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())) if (d.year != null) metadataInfoArray.add(Pair("Year", d.year.toString()))
val rating = d.rating val rating = d.rating
if (rating != null) metadataInfoArray.add(Pair("Rating", if (rating != null) metadataInfoArray.add(
"%.1f/10.0".format(rating.toFloat() / 10f).replace(",", "."))) Pair(
"Rating",
"%.1f/10.0".format(rating.toFloat() / 10f).replace(",", ".")
)
)
val duration = d.duration val duration = d.duration
if (duration != null) metadataInfoArray.add(Pair("Duration", duration)) if (duration != null) metadataInfoArray.add(Pair("Duration", duration))

View file

@ -70,9 +70,11 @@ class ResultViewModel : ViewModel() {
private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) { private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) {
_episodes.postValue(list) _episodes.postValue(list)
filterEpisodes(context, filterEpisodes(
context,
list, 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) { fun reloadEpisodes(context: Context) {
@ -119,18 +121,20 @@ class ResultViewModel : ViewModel() {
if (dataList != null) { if (dataList != null) {
val episodes = ArrayList<ResultEpisode>() val episodes = ArrayList<ResultEpisode>()
for ((index, i) in dataList.withIndex()) { for ((index, i) in dataList.withIndex()) {
episodes.add(context.buildResultEpisode( episodes.add(
i.name, context.buildResultEpisode(
i.posterUrl, i.name,
index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE i.posterUrl,
null, // TODO FIX SEASON index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE
i.url, null, // TODO FIX SEASON
apiName, i.url,
(mainId + index + 1), apiName,
index, (mainId + index + 1),
i.rating, index,
i.descript, i.rating,
)) i.descript,
)
)
} }
updateEpisodes(context, mainId, episodes, -1) updateEpisodes(context, mainId, episodes, -1)
} }
@ -139,34 +143,40 @@ class ResultViewModel : ViewModel() {
is TvSeriesLoadResponse -> { is TvSeriesLoadResponse -> {
val episodes = ArrayList<ResultEpisode>() val episodes = ArrayList<ResultEpisode>()
for ((index, i) in d.episodes.withIndex()) { for ((index, i) in d.episodes.withIndex()) {
episodes.add(context.buildResultEpisode( episodes.add(
i.name, context.buildResultEpisode(
//?: (if (i.season != null && i.episode != null) "S${i.season}:E${i.episode}" else null)), // TODO ADD NAMES i.name,
i.posterUrl, //?: (if (i.season != null && i.episode != null) "S${i.season}:E${i.episode}" else null)), // TODO ADD NAMES
i.episode ?: (index + 1), i.posterUrl,
i.season, i.episode ?: (index + 1),
i.data, i.season,
apiName, i.data,
(mainId + index + 1).hashCode(), apiName,
index, (mainId + index + 1).hashCode(),
i.rating, index,
i.descript i.rating,
)) i.descript
)
)
} }
updateEpisodes(context, mainId, episodes, -1) updateEpisodes(context, mainId, episodes, -1)
} }
is MovieLoadResponse -> { is MovieLoadResponse -> {
updateEpisodes(context, mainId, arrayListOf(context.buildResultEpisode( updateEpisodes(
null, context, mainId, arrayListOf(
null, context.buildResultEpisode(
0, null, null,
d.dataUrl, null,
d.apiName, 0, null,
(mainId + 1), d.dataUrl,
0, d.apiName,
null, (mainId + 1),
null, 0,
)), -1) null,
null,
)
), -1
)
} }
} }
} }
@ -179,17 +189,21 @@ class ResultViewModel : ViewModel() {
private val _allEpisodes: MutableLiveData<HashMap<Int, ArrayList<ExtractorLink>>> = private val _allEpisodes: MutableLiveData<HashMap<Int, ArrayList<ExtractorLink>>> =
MutableLiveData(HashMap()) // LOOKUP BY ID MutableLiveData(HashMap()) // LOOKUP BY ID
private val _allEpisodesSubs: MutableLiveData<HashMap<Int, ArrayList<SubtitleFile>>> =
MutableLiveData(HashMap()) // LOOKUP BY ID
val allEpisodes: LiveData<HashMap<Int, ArrayList<ExtractorLink>>> get() = _allEpisodes val allEpisodes: LiveData<HashMap<Int, ArrayList<ExtractorLink>>> get() = _allEpisodes
val allEpisodesSubs: LiveData<HashMap<Int, ArrayList<SubtitleFile>>> get() = _allEpisodesSubs
private var _apiName: MutableLiveData<String> = MutableLiveData() private var _apiName: MutableLiveData<String> = MutableLiveData()
val apiName: LiveData<String> get() = _apiName val apiName: LiveData<String> get() = _apiName
data class EpisodeData(val links: ArrayList<ExtractorLink>, val subs: ArrayList<SubtitleFile>)
fun loadEpisode( fun loadEpisode(
episode: ResultEpisode, episode: ResultEpisode,
isCasting: Boolean, isCasting: Boolean,
callback: (Resource<ArrayList<ExtractorLink>>) -> Unit, callback: (Resource<EpisodeData>) -> Unit,
) { ) {
loadEpisode(episode.id, episode.data, isCasting, callback) loadEpisode(episode.id, episode.data, isCasting, callback)
} }
@ -198,25 +212,29 @@ class ResultViewModel : ViewModel() {
id: Int, id: Int,
data: String, data: String,
isCasting: Boolean, isCasting: Boolean,
callback: (Resource<ArrayList<ExtractorLink>>) -> Unit, callback: (Resource<EpisodeData>) -> Unit,
) = ) =
viewModelScope.launch { viewModelScope.launch {
if (_allEpisodes.value?.contains(id) == true) { if (_allEpisodes.value?.contains(id) == true) {
_allEpisodes.value?.remove(id) _allEpisodes.value?.remove(id)
} }
val links = ArrayList<ExtractorLink>() val links = ArrayList<ExtractorLink>()
val subs = ArrayList<SubtitleFile>()
val localData = safeApiCall { val localData = safeApiCall {
getApiFromName(_apiName.value).loadLinks(data, isCasting, { subtitleFile -> }) { getApiFromName(_apiName.value).loadLinks(data, isCasting, { subtitleFile ->
for (i in links) { if (!subs.any { it.url == subtitleFile.url }) {
if (i.url == it.url) return@loadLinks 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) callback.invoke(localData)
} }

View file

@ -7,15 +7,13 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.google.android.exoplayer2.ext.cast.CastPlayer import com.google.android.exoplayer2.ext.cast.CastPlayer
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.gms.cast.CastStatusCodes import com.google.android.gms.cast.*
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.MediaStatus.REPEAT_MODE_REPEAT_OFF 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.CastContext
import com.google.android.gms.cast.framework.media.RemoteMediaClient import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.common.api.PendingResult import com.google.android.gms.common.api.PendingResult
import com.google.android.gms.common.images.WebImage import com.google.android.gms.common.images.WebImage
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.ui.MetadataHolder import com.lagradost.cloudstream3.ui.MetadataHolder
import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
@ -27,14 +25,22 @@ object CastHelper {
private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() .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<SubtitleFile>
): MediaInfo {
val link = holder.currentLinks[index] val link = holder.currentLinks[index]
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, movieMetadata.putString(
MediaMetadata.KEY_SUBTITLE,
if (holder.isMovie) if (holder.isMovie)
link.name link.name
else else
(epData.name ?: "Episode ${epData.episode}") + " - ${link.name}") (epData.name ?: "Episode ${epData.episode}") + " - ${link.name}"
)
movieMetadata.putString(MediaMetadata.KEY_TITLE, holder.title) movieMetadata.putString(MediaMetadata.KEY_TITLE, holder.title)
@ -43,11 +49,21 @@ object CastHelper {
movieMetadata.addImage(WebImage(Uri.parse(srcPoster))) 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) return MediaInfo.Builder(link.url)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(MimeTypes.VIDEO_UNKNOWN) .setContentType(MimeTypes.VIDEO_UNKNOWN)
.setCustomData(data) .setCustomData(data)
.setMetadata(movieMetadata) .setMetadata(movieMetadata)
.setMediaTracks(tracks)
.build() .build()
} }
@ -76,39 +92,48 @@ object CastHelper {
currentEpisodeIndex: Int, currentEpisodeIndex: Int,
episodes: List<ResultEpisode>, episodes: List<ResultEpisode>,
currentLinks: List<ExtractorLink>, currentLinks: List<ExtractorLink>,
subtitles: List<SubtitleFile>,
startIndex: Int? = null, startIndex: Int? = null,
startTime: Long? = null, startTime: Long? = null,
) { ) : Boolean {
if (episodes.isEmpty()) return if (episodes.isEmpty()) return false
if (currentLinks.size <= currentEpisodeIndex) return false
val castContext = CastContext.getSharedInstance(this) val castContext = CastContext.getSharedInstance(this)
val epData = episodes[currentEpisodeIndex] 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 index = startIndex ?: 0
val mediaItem = val mediaItem =
getMediaInfo(epData, holder, index, JSONObject(mapper.writeValueAsString(holder))) getMediaInfo(epData, holder, index, JSONObject(mapper.writeValueAsString(holder)), subtitles)
val castPlayer = CastPlayer(castContext) val castPlayer = CastPlayer(castContext)
castPlayer.repeatMode = REPEAT_MODE_REPEAT_OFF castPlayer.repeatMode = REPEAT_MODE_REPEAT_OFF
awaitLinks(castPlayer.loadItem( awaitLinks(
MediaQueueItem.Builder(mediaItem).build(), castPlayer.loadItem(
startTime ?: 0, MediaQueueItem.Builder(mediaItem).build(),
)) { startTime ?: 0,
)
) {
if (currentLinks.size > index + 1) if (currentLinks.size > index + 1)
startCast(apiName, startCast(
apiName,
isMovie, isMovie,
title, title,
poster, poster,
currentEpisodeIndex, currentEpisodeIndex,
episodes, episodes,
currentLinks, currentLinks,
subtitles,
index + 1, index + 1,
startTime) startTime
)
} }
return true
} }
} }

View file

@ -91,6 +91,6 @@ object DataStore {
} }
inline fun <reified T : Any> Context.getKey(folder: String, path: String, defVal: T?): T? { inline fun <reified T : Any> Context.getKey(folder: String, path: String, defVal: T?): T? {
return getKey(getFolderName(folder, path), defVal) return getKey(getFolderName(folder, path), defVal) ?: defVal
} }
} }

View file

@ -1,48 +1,69 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:background="@null" android:background="@null"
android:layout_height="match_parent"> android:layout_height="match_parent">
<!--<androidx.cardview.widget.CardView
app:cardCornerRadius="10dp"
android:backgroundTint="?attr/boxItemBackground"
android:layout_width="match_parent" android:layout_height="wrap_content">
<TextView
style="@style/AppTextViewStyle"
android:gravity="center_vertical"
android:paddingTop="5dp" <LinearLayout
android:paddingBottom="5dp" android:layout_width="match_parent"
android:paddingLeft="20dp" android:layout_height="0dp"
android:paddingRight="20dp" android:orientation="vertical"
android:layout_gravity="center_vertical" android:layout_weight="50">
android:text="@string/pick_source" android:textColor="?attr/textColor" android:textSize="18sp" <TextView
android:layout_width="wrap_content" android:layout_height="55dp" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
> android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:textStyle="bold"
android:text="@string/pick_source"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content">
</TextView> </TextView>
</androidx.cardview.widget.CardView>--> <ListView
<TextView android:layout_marginTop="-10dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingTop="10dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:id="@+id/sort_providers"
android:layout_marginTop="20dp" android:background="?attr/bitDarkerGrayBackground"
android:layout_marginBottom="10dp" tools:listitem="@layout/sort_bottom_single_choice"
android:textStyle="bold" android:layout_width="match_parent"
android:text="@string/pick_source" android:layout_height="match_parent"
android:textSize="20sp" android:layout_rowWeight="1"
android:textColor="?attr/textColor" />
</LinearLayout>
<LinearLayout
android:id="@+id/sort_subtitles_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="0dp"
</TextView> android:orientation="vertical"
<ListView android:layout_weight="50">
android:layout_marginTop="-10dp" <TextView
android:paddingTop="10dp" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:id="@+id/sort_click" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:background="?attr/bitDarkerGrayBackground" android:layout_marginTop="20dp"
tools:listitem="@layout/sort_bottom_single_choice" android:layout_marginBottom="10dp"
android:layout_width="match_parent" android:textStyle="bold"
android:layout_height="wrap_content"> android:text="@string/pick_subtitle"
</ListView> android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_rowWeight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</TextView>
<ListView
android:layout_marginTop="-10dp"
android:paddingTop="10dp"
android:id="@+id/sort_subtitles"
android:background="?attr/bitDarkerGrayBackground"
tools:listitem="@layout/sort_bottom_single_choice"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -31,7 +31,8 @@
<string name="type_plan_to_watch">Plan to Watch</string> <string name="type_plan_to_watch">Plan to Watch</string>
<string name="type_none">None</string> <string name="type_none">None</string>
<string name="play_movie_button">Play Movie</string> <string name="play_movie_button">Play Movie</string>
<string name="pick_source">Pick Source</string> <string name="pick_source">Sources</string>
<string name="pick_subtitle">Subtitles</string>
<string name="reload_error">Retry connection…</string> <string name="reload_error">Retry connection…</string>
<string name="result_go_back">Go Back</string> <string name="result_go_back">Go Back</string>
<string name="episode_poster">Episode Poster</string> <string name="episode_poster">Episode Poster</string>

View file

@ -118,6 +118,15 @@
<style name="AlertDialogCustomTransparent" parent="Theme.AppCompat.Dialog.Alert"> <style name="AlertDialogCustomTransparent" parent="Theme.AppCompat.Dialog.Alert">
<item name="android:windowBackground">@color/transparent</item> <item name="android:windowBackground">@color/transparent</item>
</style> </style>
<style name="AlertDialogCustomBlack" parent="Theme.AppCompat.Dialog.Alert">
<item name="android:windowBackground">@color/bitDarkerGrayBackground</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">fill_parent</item>
<!-- No backgrounds, titles or window float -->
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">false</item>
<item name="android:navigationBarColor">@color/bitDarkerGrayBackground</item>
</style>
<style name="PopupMenu" parent="@android:style/Widget.PopupMenu"> <style name="PopupMenu" parent="@android:style/Widget.PopupMenu">
<item name="android:backgroundTint">?attr/bitDarkerGrayBackground</item> <item name="android:backgroundTint">?attr/bitDarkerGrayBackground</item>