Merge branch 'master' into githubAccount

This commit is contained in:
Cloudburst 2022-10-08 22:48:57 +02:00 committed by GitHub
commit 5dc3807c96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 546 additions and 200 deletions

View file

@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Report provider bug
- name: Request a new provider or report bug with an existing provider
url: https://github.com/recloudstream
about: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
- name: Discord
url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues.

View file

@ -2,7 +2,7 @@ name: Issue automatic actions
on:
issues:
types: [opened, edited]
types: [opened]
jobs:
issue-moderator:

View file

@ -21,6 +21,12 @@
android:name="android.software.leanback"
android:required="false" />
<queries>
<package android:name="org.videolan.vlc" />
<package android:name="com.instantbits.cast.webvideo" />
<package android:name="is.xyz.mpv" />
</queries>
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
<application
android:name=".AcraApplication"

View file

@ -10,16 +10,22 @@ import android.util.Log
import android.view.*
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.MainThread
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
@ -34,6 +40,7 @@ object CommonActivity {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
}
var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false
@ -117,7 +124,7 @@ object CommonActivity {
setLocale(this, localeCode)
}
fun init(act: Activity?) {
fun init(act: ComponentActivity?) {
if (act == null) return
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture
@ -129,6 +136,22 @@ object CommonActivity {
act.updateLocale()
act.updateTv()
NewPipe.init(DownloaderTestImpl.getInstance())
for (resumeApp in resumeApps) {
resumeApp.launcher =
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
val pos = data.getLongExtra(resumeApp.position, -1L)
val dur = data.getLongExtra(resumeApp.duration, -1L)
if (dur > 0L && pos > 0L)
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
removeKey(resumeApp.lastId)
ResultFragment.updateUI()
}
}
}
}
private fun Activity.enterPIPMode() {
@ -167,7 +190,7 @@ object CommonActivity {
"Amoled" -> R.style.AmoledMode
"AmoledLight" -> R.style.AmoledModeLight
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.MonetMode else R.style.AppTheme
R.style.MonetMode else R.style.AppTheme
else -> R.style.AppTheme
}

View file

@ -10,7 +10,7 @@ import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
@ -36,6 +36,8 @@ import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.initAll
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
@ -55,7 +57,6 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.githubA
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
@ -71,10 +72,8 @@ import com.lagradost.cloudstream3.utils.BackupUtils.restorePromptGithub
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.IOnBackPressed
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
@ -99,17 +98,65 @@ import java.nio.charset.Charset
import kotlin.reflect.KClass
const val VLC_PACKAGE = "org.videolan.vlc"
const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
val VLC_COMPONENT: ComponentName =
ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity")
const val VLC_REQUEST_CODE = 42
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
//https://wiki.videolan.org/Android_Player_Intents/
const val VLC_FROM_START = -1
const val VLC_FROM_PROGRESS = -2
const val VLC_EXTRA_POSITION_OUT = "extra_position"
const val VLC_EXTRA_DURATION_OUT = "extra_duration"
const val VLC_LAST_ID_KEY = "vlc_last_open_id"
//https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904
//https://mpv-android.github.io/mpv-android/intent.html
// https://www.webvideocaster.com/integrations
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
const val VLC_PACKAGE = "org.videolan.vlc"
const val MPV_PACKAGE = "is.xyz.mpv"
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
//TODO REFACTOR AF
data class ResultResume(
val packageString: String,
val action: String = Intent.ACTION_VIEW,
val position: String? = null,
val duration: String? = null,
var launcher: ActivityResultLauncher<Intent>? = null,
) {
val lastId get() = "${packageString}_last_open_id"
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
val intent = Intent(action)
if (id != null)
setKey(lastId, id)
else
removeKey(lastId)
intent.setPackage(packageString)
callback.invoke(intent)
launcher?.launch(intent)
}
}
val VLC = ResultResume(
VLC_PACKAGE,
"org.videolan.vlc.player.result",
"extra_position",
"extra_duration",
)
val MPV = ResultResume(
MPV_PACKAGE,
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
position = "position",
duration = "duration",
)
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
val resumeApps = arrayOf(
VLC, MPV, WEB_VIDEO
)
// Short name for requests client to make it nicer to use
@ -376,31 +423,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == VLC_REQUEST_CODE) {
if (resultCode == RESULT_OK && data != null) {
val pos: Long =
data.getLongExtra(
VLC_EXTRA_POSITION_OUT,
-1
) //Last position in media when player exited
val dur: Long =
data.getLongExtra(
VLC_EXTRA_DURATION_OUT,
-1
) //Last position in media when player exited
val id = getKey<Int>(VLC_LAST_ID_KEY)
println("SET KEY $id at $pos / $dur")
if (dur > 0 && pos > 0) {
setViewPos(id, pos, dur)
}
removeKey(VLC_LAST_ID_KEY)
ResultFragment.updateUI()
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onDestroy() {
val broadcastIntent = Intent()
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)

View file

@ -0,0 +1,39 @@
package com.lagradost.cloudstream3.extractors
import android.util.Log
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.*
class AStreamHub : ExtractorApi() {
override val name = "AStreamHub"
override val mainUrl = "https://astreamhub.com"
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
app.get(url).document.selectFirst("body > script").let { script ->
val text = script?.html() ?: ""
Log.i("Dev", "text => $text")
if (text.isNotBlank()) {
val m3link = "(?<=file:)(.*)(?=,)".toRegex().find(text)
?.groupValues?.get(0)?.trim()?.trim('"') ?: ""
Log.i("Dev", "m3link => $m3link")
if (m3link.isNotBlank()) {
sources.add(
ExtractorLink(
name = name,
source = name,
url = m3link,
isM3u8 = true,
quality = Qualities.Unknown.value,
referer = referer ?: url
)
)
}
}
}
return sources
}
}

View file

@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
@ -123,6 +124,10 @@ object PluginManager {
val plugins = getPluginsOnline().filter {
!it.filePath.contains(repositoryPath)
}
val file = File(repositoryPath)
normalSafeApiCall {
if (file.exists()) file.deleteRecursively()
}
setKey(PLUGINS_KEY, plugins)
}
}
@ -174,8 +179,16 @@ object PluginManager {
val onlineData: Pair<String, SitePlugin>,
) {
val isOutdated =
onlineData.second.version != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN
fun validOnlineData(context: Context): Boolean {
return getPluginPath(
context,
savedData.internalName,
onlineData.first
).absolutePath == savedData.filePath
}
}
// var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
@ -225,6 +238,8 @@ object PluginManager {
.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
.map { onlineData ->
OnlinePluginData(savedData, onlineData)
}.filter {
it.validOnlineData(activity)
}
}.flatten().distinctBy { it.onlineData.second.url }
@ -416,6 +431,18 @@ object PluginManager {
) + "." + name.hashCode()
}
/**
* This should not be changed as it is used to also detect if a plugin is installed!
**/
fun getPluginPath(
context: Context,
internalName: String,
repositoryUrl: String
): File {
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
val fileName = getPluginSanitizedFileName(internalName)
return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
}
/**
* Used for fresh installs
@ -426,9 +453,7 @@ object PluginManager {
internalName: String,
repositoryUrl: String
): Boolean {
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
val fileName = getPluginSanitizedFileName(internalName)
val file = File("${activity.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
val file = getPluginPath(activity, internalName, repositoryUrl)
downloadAndLoadPlugin(activity, pluginUrl, internalName, file)
return true
}
@ -454,7 +479,13 @@ object PluginManager {
return loadPlugin(
activity,
newFile ?: return false,
PluginData(internalName, pluginUrl, true, newFile.absolutePath, PLUGIN_VERSION_NOT_SET)
PluginData(
internalName,
pluginUrl,
true,
newFile.absolutePath,
PLUGIN_VERSION_NOT_SET
)
)
} catch (e: Exception) {
logError(e)
@ -462,18 +493,13 @@ object PluginManager {
}
}
/**
* @param isFilePath will treat the pluginUrl as as the filepath instead of url
* */
suspend fun deletePlugin(pluginIdentifier: String, isFilePath: Boolean): Boolean {
val data =
(if (isFilePath) (getPluginsLocal() + getPluginsOnline()).firstOrNull { it.filePath == pluginIdentifier }
else getPluginsOnline().firstOrNull { it.url == pluginIdentifier }) ?: return false
suspend fun deletePlugin(file: File): Boolean {
val list = (getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
return try {
if (File(data.filePath).delete()) {
unloadPlugin(data.filePath)
deletePluginData(data)
if (File(file.absolutePath).delete()) {
unloadPlugin(file.absolutePath)
list.forEach { deletePluginData(it) }
return true
}
false

View file

@ -244,7 +244,7 @@ class IndexSubtitleApi : AbstractSubApi {
} else {
document.select("div.my-3.p-3 div.media").mapNotNull { block ->
val name =
block.selectFirst("strong.d-block.text-primary")?.text()?.trim().toString()
block.selectFirst("strong.d-block")?.text()?.trim().toString()
if (seasonNum!! > 0) {
if (isRightEps(name, seasonNum, epNum)) {
fixUrl(block.selectFirst("a")!!.attr("href"))

View file

@ -521,15 +521,12 @@ class HomeFragment : Fragment() {
}
}
//Disable Random button, if its toggled off on settings
//Load value for toggling Random button. Hide at startup
context?.let {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
toggleRandomButton =
settingsManager.getBoolean(getString(R.string.random_button_key), false)
home_random?.isVisible = toggleRandomButton
if (!toggleRandomButton) {
home_random?.visibility = View.GONE
}
home_random?.visibility = View.GONE
}
observe(homeViewModel.apiName) { apiName ->
@ -626,6 +623,7 @@ class HomeFragment : Fragment() {
home_loading_shimmer?.stopShimmer()
val d = data.value
val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear()
// println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}")
@ -638,6 +636,11 @@ class HomeFragment : Fragment() {
home_loading_error?.isVisible = false
home_loaded?.isVisible = true
if (toggleRandomButton) {
//Flatten list
d.values.forEach { dlist ->
mutableListOfResponse.addAll(dlist.list.list)
}
listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url })
home_random?.isVisible = listHomepageItems.isNotEmpty()
} else {
home_random?.isGone = true

View file

@ -276,9 +276,6 @@ class HomeViewModel : ViewModel() {
if (preferredApiName == noneApi.name) {
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
loadAndCancel(noneApi)
// If the plugin isn't loaded yet. (Does not set the key)
} else if (api == null) {
loadAndCancel(noneApi)
} else if (preferredApiName == randomApi.name) {
val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) {
@ -289,6 +286,9 @@ class HomeViewModel : ViewModel() {
loadAndCancel(apiRandom)
setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
}
// If the plugin isn't loaded yet. (Does not set the key)
} else if (api == null) {
loadAndCancel(noneApi)
} else {
setKey(USER_SELECTED_HOMEPAGE_API, api.name)
loadAndCancel(api)

View file

@ -58,6 +58,7 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.*
import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings
import kotlinx.android.synthetic.main.player_select_tracks.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class GeneratorPlayer : FullScreenPlayer() {
companion object {
@ -115,10 +116,11 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onTracksInfoChanged() {
val tracks = player.getVideoTracks()
player_tracks_btt?.isVisible = tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
player_tracks_btt?.isVisible =
tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
// Only set the preferred language if it is available.
// Otherwise it may give some users audio track init failed!
if (tracks.allAudioTracks.any { it.language == preferredAudioTrackLanguage }){
if (tracks.allAudioTracks.any { it.language == preferredAudioTrackLanguage }) {
player.setPreferredAudioTrack(preferredAudioTrackLanguage)
}
}
@ -602,8 +604,20 @@ class GeneratorPlayer : FullScreenPlayer() {
subtitleList.setItemChecked(subtitleIndex, true)
subtitleList.setOnItemClickListener { _, _, which, _ ->
subtitleIndex = which
subtitleList.setItemChecked(which, true)
if (which > currentSubtitles.size) {
// Since android TV is funky the setOnItemClickListener will be triggered
// instead of setOnClickListener when selecting. To override this we programmatically
// click the view when selecting an item outside the list.
// Cheeky way of getting the view at that position to click it
// to avoid keeping track of the various footers.
// getChildAt() gives null :(
val child = subtitleList.adapter.getView(which, null, subtitleList)
child?.performClick()
} else {
subtitleIndex = which
subtitleList.setItemChecked(which, true)
}
}
sourceDialog.cancel_btt?.setOnClickListener {
@ -762,7 +776,8 @@ class GeneratorPlayer : FullScreenPlayer() {
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
// audioArrayAdapter.add(ctx.getString(R.string.no_subtitles))
audioArrayAdapter.addAll(currentAudioTracks.mapIndexed { index, format ->
format.label ?: format.language?.let { fromTwoLettersToLanguage(it) } ?: index.toString()
format.label ?: format.language?.let { fromTwoLettersToLanguage(it) }
?: index.toString()
})
audioList.adapter = audioArrayAdapter

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -9,6 +10,7 @@ import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
@ -53,6 +55,10 @@ const val ACTION_SHOW_DESCRIPTION = 15
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
const val ACTION_PLAY_EPISODE_IN_MPV = 17
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
class EpisodeAdapter(
@ -60,6 +66,25 @@ class EpisodeAdapter(
private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
/**
* @return ACTION_PLAY_EPISODE_IN_PLAYER, ACTION_PLAY_EPISODE_IN_BROWSER or ACTION_PLAY_EPISODE_IN_VLC_PLAYER depending on player settings.
* See array.xml/player_pref_values
**/
fun getPlayerAction(context: Context): Int {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
return when (settingsManager.getInt(context.getString(R.string.player_pref_key), 1)) {
1 -> ACTION_PLAY_EPISODE_IN_PLAYER
2 -> ACTION_PLAY_EPISODE_IN_VLC_PLAYER
3 -> ACTION_PLAY_EPISODE_IN_BROWSER
4 -> ACTION_PLAY_EPISODE_IN_WEB_VIDEO
5 -> ACTION_PLAY_EPISODE_IN_MPV
else -> ACTION_PLAY_EPISODE_IN_PLAYER
}
}
}
var cardList: MutableList<ResultEpisode> = mutableListOf()
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()

View file

@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.*
@ -455,7 +456,8 @@ open class ResultFragment : ResultTrailerPlayer() {
val apiName: String,
val showFillers: Boolean,
val dubStatus: DubStatus,
val start: AutoResume?
val start: AutoResume?,
val playerAction: Int
)
private fun getStoredData(context: Context): StoredData? {
@ -469,6 +471,8 @@ open class ResultFragment : ResultTrailerPlayer() {
) DubStatus.Dubbed else DubStatus.Subbed
val startAction = arguments?.getInt(START_ACTION_BUNDLE)
val playerAction = getPlayerAction(context)
val start = startAction?.let { action ->
val startValue = arguments?.getInt(START_VALUE_BUNDLE)
val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE)
@ -483,7 +487,7 @@ open class ResultFragment : ResultTrailerPlayer() {
season = resumeSeason
)
}
return StoredData(url, apiName, showFillers, dubStatus, start)
return StoredData(url, apiName, showFillers, dubStatus, start, playerAction)
}
private fun reloadViewModel(success: Boolean = false) {
@ -774,7 +778,8 @@ open class ResultFragment : ResultTrailerPlayer() {
viewModel.handleAction(
activity,
EpisodeClickEvent(
ACTION_PLAY_EPISODE_IN_PLAYER, value.result
storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER,
value.result
)
)
}

View file

@ -1,14 +1,13 @@
package com.lagradost.cloudstream3.ui.result
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.*
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -16,6 +15,7 @@ import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.getCastSession
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.IGenerator
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
@ -43,7 +44,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
@ -52,9 +52,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import kotlinx.coroutines.*
import java.io.File
import java.lang.Math.abs
@ -615,7 +613,7 @@ class ResultViewModel2 : ViewModel() {
val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let
// SET VISUAL KEYS
AcraApplication.setKey(
setKey(
DOWNLOAD_HEADER_CACHE,
parentId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
@ -629,7 +627,7 @@ class ResultViewModel2 : ViewModel() {
)
)
AcraApplication.setKey(
setKey(
DataStore.getFolderName(
DOWNLOAD_EPISODE_CACHE,
parentId.toString()
@ -956,70 +954,155 @@ class ResultViewModel2 : ViewModel() {
return LinkLoadingResult(sortUrls(links), sortSubs(subs))
}
private fun playWithVlc(act: Activity?, data: LinkLoadingResult, id: Int) = ioSafe {
if (act == null) return@ioSafe
if (data.links.isEmpty()) {
showToast(act, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
return@ioSafe
private fun launchActivity(
activity: Activity?,
resumeApp: ResultResume,
id: Int? = null,
work: suspend (Intent.(Activity) -> Unit)
): Job? {
val act = activity ?: return null
return CoroutineScope(Dispatchers.IO).launch {
try {
resumeApp.launch(id) {
work(act)
}
} catch (t: Throwable) {
logError(t)
main {
if (t is ActivityNotFoundException) {
showToast(activity, txt(R.string.app_not_found_error), Toast.LENGTH_LONG)
} else {
showToast(activity, t.toString(), Toast.LENGTH_LONG)
}
}
}
}
try {
if (!act.checkWrite()) {
act.requestRW()
if (act.checkWrite()) return@ioSafe
}
}
val outputDir = act.cacheDir
val outputFile = withContext(Dispatchers.IO) {
File.createTempFile("mirrorlist", ".m3u8", outputDir)
private fun playInWebVideo(
activity: Activity?,
link: ExtractorLink,
title: String?,
posterUrl: String?,
subtitles: List<SubtitleData>
) = launchActivity(activity, WEB_VIDEO) {
setDataAndType(Uri.parse(link.url), "video/*")
putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray())
title?.let { putExtra("title", title) }
posterUrl?.let { putExtra("poster", posterUrl) }
val headers = Bundle().apply {
if (link.referer.isNotBlank())
putString("Referer", link.referer)
putString("User-Agent", USER_AGENT)
for ((key, value) in link.headers) {
putString(key, value)
}
}
putExtra("android.media.intent.extra.HTTP_HEADERS", headers)
putExtra("secure_uri", true)
}
private fun playWithMpv(
activity: Activity?,
id: Int,
link: ExtractorLink,
subtitles: List<SubtitleData>,
resume: Boolean = true,
) = launchActivity(activity, MPV, id) {
putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray())
putExtra("subs.name", subtitles.map { it.name }.toTypedArray())
putExtra("subs.filename", subtitles.map { it.name }.toTypedArray())
setDataAndType(Uri.parse(link.url), "video/*")
component = MPV_COMPONENT
putExtra("secure_uri", true)
putExtra("return_result", true)
val position = getViewPos(id)?.position
if (resume && position != null)
putExtra("position", position.toInt())
}
// https://wiki.videolan.org/Android_Player_Intents/
private fun playWithVlc(
activity: Activity?,
data: LinkLoadingResult,
id: Int,
resume: Boolean = true,
// if it is only a single link then resume works correctly
singleFile: Boolean? = null
) = launchActivity(activity, VLC, id) { act ->
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val outputDir = act.cacheDir
if (singleFile ?: (data.links.size == 1)) {
setDataAndType(data.links.first().url.toUri(), "video/*")
} else {
val outputFile = File.createTempFile("mirrorlist", ".m3u8", outputDir)
var text = "#EXTM3U"
for (sub in data.subs) {
text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\""
}
// With subtitles it doesn't work for no reason :(
// for (sub in data.subs) {
// text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\""
// }
for (link in data.links) {
text += "\n#EXTINF:, ${link.name}\n${link.url}"
}
outputFile.writeText(text)
val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT)
vlcIntent.setPackage(VLC_PACKAGE)
vlcIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
vlcIntent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
vlcIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
vlcIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
vlcIntent.setDataAndType(
setDataAndType(
FileProvider.getUriForFile(
act,
act.applicationContext.packageName + ".provider",
outputFile
), "video/*"
)
val startId = VLC_FROM_PROGRESS
var position = startId
if (startId == VLC_FROM_START) {
position = 1
} else if (startId == VLC_FROM_PROGRESS) {
position = 0
}
vlcIntent.putExtra("position", position)
vlcIntent.component = VLC_COMPONENT
act.setKey(VLC_LAST_ID_KEY, id)
act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE)
} catch (e: Exception) {
logError(e)
showToast(act, e.toString(), Toast.LENGTH_LONG)
}
val position = if (resume) {
getViewPos(id)?.position ?: 0L
} else {
1L
}
component = VLC_COMPONENT
putExtra("from_start", !resume)
putExtra("position", position)
}
fun handleAction(activity: Activity?, click: EpisodeClickEvent) = viewModelScope.launchSafe {
handleEpisodeClickEvent(activity, click)
}
fun handleAction(activity: Activity?, click: EpisodeClickEvent) =
viewModelScope.launchSafe {
handleEpisodeClickEvent(activity, click)
}
data class ExternalApp(
val packageString: String,
val name: Int,
val action: Int,
)
private val apps = listOf(
ExternalApp(
VLC_PACKAGE,
R.string.player_settings_play_in_vlc,
ACTION_PLAY_EPISODE_IN_VLC_PLAYER
), ExternalApp(
WEB_VIDEO_CAST_PACKAGE,
R.string.player_settings_play_in_web,
ACTION_PLAY_EPISODE_IN_WEB_VIDEO
),
ExternalApp(
MPV_PACKAGE,
R.string.player_settings_play_in_mpv,
ACTION_PLAY_EPISODE_IN_MPV
)
)
private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) {
when (click.action) {
@ -1035,9 +1118,17 @@ class ResultViewModel2 : ViewModel() {
}
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
if (activity?.isAppInstalled(VLC_PACKAGE) == true) {
options.add(txt(R.string.episode_action_play_in_vlc) to ACTION_PLAY_EPISODE_IN_VLC_PLAYER)
for (app in apps) {
if (activity?.isAppInstalled(app.packageString) == true) {
options.add(
txt(
R.string.episode_action_play_in_format,
txt(app.name)
) to app.action
)
}
}
options.addAll(
listOf(
txt(R.string.episode_action_play_in_browser) to ACTION_PLAY_EPISODE_IN_BROWSER,
@ -1073,9 +1164,10 @@ class ResultViewModel2 : ViewModel() {
click.copy(action = ACTION_CHROME_CAST_EPISODE)
)
} else {
val action = getPlayerAction(ctx)
handleEpisodeClickEvent(
activity,
click.copy(action = ACTION_PLAY_EPISODE_IN_PLAYER)
click.copy(action = action)
)
}
}
@ -1212,6 +1304,11 @@ class ResultViewModel2 : ViewModel() {
}
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
loadLinks(click.data, isVisible = true, isCasting = true) { links ->
if (links.links.isEmpty()) {
showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
return@loadLinks
}
playWithVlc(
activity,
links,
@ -1219,6 +1316,37 @@ class ResultViewModel2 : ViewModel() {
)
}
}
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
click.data,
isCasting = true,
txt(
R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_web)
)
) { (result, index) ->
playInWebVideo(
activity,
result.links[index],
click.data.name ?: click.data.headerName,
click.data.poster,
result.subs
)
}
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
click.data,
isCasting = true,
txt(
R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_mpv)
)
) { (result, index) ->
playWithMpv(
activity,
click.data.id,
result.links[index],
result.subs
)
}
ACTION_PLAY_EPISODE_IN_PLAYER -> {
val data = currentResponse?.syncData?.toList() ?: emptyList()
val list =
@ -1284,7 +1412,11 @@ class ResultViewModel2 : ViewModel() {
}, {
if (this !is AnimeLoadResponse) return@argamap
val map =
Kitsu.getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false)
Kitsu.getEpisodesDetails(
getMalId(),
getAniListId(),
isResponseRequired = false
)
if (map.isNullOrEmpty()) return@argamap
updateEpisodes = DubStatus.values().map { dubStatus ->
val current =
@ -1304,8 +1436,10 @@ class ResultViewModel2 : ViewModel() {
val currentBack = this
this.description = this.description ?: node.description?.en
this.name = this.name ?: node.titles?.canonical
this.episode = this.episode ?: node.num ?: episodeNumbers[index]
this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url
this.episode =
this.episode ?: node.num ?: episodeNumbers[index]
this.posterUrl =
this.posterUrl ?: node.thumbnail?.original?.url
}
}
}
@ -1592,7 +1726,9 @@ class ResultViewModel2 : ViewModel() {
val idIndex = ep.key.id
for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1)
val id = mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) ?: 0)
val id =
mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000)
?: 0)
if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season)
@ -1888,7 +2024,10 @@ class ResultViewModel2 : ViewModel() {
if (ep.getWatchProgress() > 0.9) continue
handleAction(
activity,
EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep)
EpisodeClickEvent(
getPlayerAction(activity),
ep
)
)
break
}
@ -1905,7 +2044,10 @@ class ResultViewModel2 : ViewModel() {
?: return@launchSafe
handleAction(
activity,
EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, episode)
EpisodeClickEvent(
getPlayerAction(activity),
episode
)
)
}
}
@ -1983,7 +2125,7 @@ class ResultViewModel2 : ViewModel() {
preferStartEpisode = getResultEpisode(mainId)
preferStartSeason = getResultSeason(mainId)
AcraApplication.setKey(
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(

View file

@ -113,6 +113,22 @@ class SettingsPlayer : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
getPref(R.string.player_pref_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.player_pref_names)
val prefValues = resources.getIntArray(R.array.player_pref_values)
val current = settingsManager.getInt(getString(R.string.player_pref_key), 1)
activity?.showBottomDialog(
prefNames.toList(),
prefValues.indexOf(current),
getString(R.string.player_pref),
true,
{}) {
settingsManager.edit().putInt(getString(R.string.player_pref_key), prefValues[it]).apply()
}
return@setOnPreferenceClickListener true
}
getPref(R.string.subtitle_settings_key)?.setOnPreferenceClickListener {
SubtitlesFragment.push(activity, false)
return@setOnPreferenceClickListener true

View file

@ -147,7 +147,7 @@ class PluginsFragment : Fragment() {
pluginViewModel.updatePluginListLocal()
tv_types_scroll_view?.isVisible = false
} else {
pluginViewModel.updatePluginList(url)
pluginViewModel.updatePluginList(context, url)
tv_types_scroll_view?.isVisible = true
// 💀💀💀💀💀💀💀 Recyclerview when

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.settings.extensions
import android.app.Activity
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LiveData
@ -13,6 +14,7 @@ import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.plugins.PluginData
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath
import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.plugins.SitePlugin
import com.lagradost.cloudstream3.ui.result.txt
@ -45,8 +47,8 @@ class PluginsViewModel : ViewModel() {
private val repositoryCache: MutableMap<String, List<Plugin>> = mutableMapOf()
const val TAG = "PLG"
private fun isDownloaded(plugin: Plugin, data: Set<String>? = null): Boolean {
return (data ?: getDownloads()).contains(plugin.second.internalName)
private fun isDownloaded(context: Context, pluginName: String, repositoryUrl: String): Boolean {
return getPluginPath(context, pluginName, repositoryUrl).exists()
}
private suspend fun getPlugins(
@ -63,24 +65,15 @@ class PluginsViewModel : ViewModel() {
?.also { repositoryCache[repositoryUrl] = it } ?: emptyList()
}
private fun getStoredPlugins(): Array<PluginData> {
return PluginManager.getPluginsOnline()
}
private fun getDownloads(): Set<String> {
return getStoredPlugins().map { it.internalName }.toSet()
}
/**
* @param viewModel optional, updates the plugins livedata for that viewModel if included
* */
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) =
ioSafe {
if (activity == null) return@ioSafe
val stored = getDownloads()
val plugins = getPlugins(repositoryUrl)
plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list ->
plugins.filter { plugin -> !isDownloaded(activity, plugin.second.internalName, repositoryUrl) }.also { list ->
main {
showToast(
activity,
@ -103,7 +96,7 @@ class PluginsViewModel : ViewModel() {
PluginManager.downloadAndLoadPlugin(
activity,
metadata.url,
metadata.name,
metadata.internalName,
repo
)
}.main { list ->
@ -117,7 +110,7 @@ class PluginsViewModel : ViewModel() {
),
Toast.LENGTH_SHORT
)
viewModel?.updatePluginListPrivate(repositoryUrl)
viewModel?.updatePluginListPrivate(activity, repositoryUrl)
} else if (list.isNotEmpty()) {
showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT)
}
@ -140,11 +133,10 @@ class PluginsViewModel : ViewModel() {
if (activity == null) return@ioSafe
val (repo, metadata) = plugin
val (success, message) = if (isDownloaded(plugin) || isLocal) {
PluginManager.deletePlugin(
metadata.url,
isLocal
) to R.string.plugin_deleted
val file = getPluginPath(activity, plugin.second.internalName, plugin.first)
val (success, message) = if (file.exists() || isLocal) {
PluginManager.deletePlugin(file) to R.string.plugin_deleted
} else {
PluginManager.downloadAndLoadPlugin(
activity,
@ -165,14 +157,13 @@ class PluginsViewModel : ViewModel() {
if (isLocal)
updatePluginListLocal()
else
updatePluginListPrivate(repositoryUrl)
updatePluginListPrivate(activity, repositoryUrl)
}
private suspend fun updatePluginListPrivate(repositoryUrl: String) {
val stored = getDownloads()
private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) {
val plugins = getPlugins(repositoryUrl)
val list = plugins.map { plugin ->
PluginViewData(plugin, isDownloaded(plugin, stored))
PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first))
}
this.plugins = list
@ -211,9 +202,10 @@ class PluginsViewModel : ViewModel() {
_filteredPlugins.postValue(false to plugins.filterTvTypes().filterLang().sortByQuery(currentQuery))
}
fun updatePluginList(repositoryUrl: String) = viewModelScope.launchSafe {
fun updatePluginList(context: Context?, repositoryUrl: String) = viewModelScope.launchSafe {
if (context == null) return@launchSafe
Log.i(TAG, "updatePluginList = $repositoryUrl")
updatePluginListPrivate(repositoryUrl)
updatePluginListPrivate(context, repositoryUrl)
}
fun search(query: String?) {

View file

@ -135,7 +135,8 @@ class SubtitlesFragment : Fragment() {
it.mkdir()
}
return fontDir.list()?.mapNotNull {
if (it.endsWith(".ttf")) {
// No idea which formats are supported, but these should be.
if (it.endsWith(".ttf") || it.endsWith(".otf")) {
File(fontDir.absolutePath + "/" + it)
} else null
} ?: listOf()

View file

@ -344,6 +344,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
VidSrcExtractor(),
VidSrcExtractor2(),
PlayLtXyz(),
AStreamHub(),
)

View file

@ -274,7 +274,7 @@
<string name="episode_action_chromecast_episode">حلقة كروم كاست</string>
<string name="episode_action_chromecast_mirror">مرآة كروم كاست</string>
<string name="episode_action_play_in_app">تشغيل في التطبيق</string>
<string name="episode_action_play_in_vlc">VLC تشغيل في</string>
<string name="episode_action_play_in_format">%s تشغيل في</string>
<string name="episode_action_play_in_browser">تشغيل في الويب </string>
<string name="episode_action_copy_link">نسخ الرابط</string>
<string name="episode_action_auto_download">التحميل التلقائي</string>

View file

@ -279,7 +279,7 @@
<string name="episode_action_chromecast_episode">Episódio pelo Chromecast</string>
<string name="episode_action_chromecast_mirror">Alternativa pelo Chromecast</string>
<string name="episode_action_play_in_app">Assistir no App</string>
<string name="episode_action_play_in_vlc">Assistir no VLC</string>
<string name="episode_action_play_in_format">Assistir no %s</string>
<string name="episode_action_play_in_browser">Assistir no navegador</string>
<string name="episode_action_copy_link">Copiar link</string>
<string name="episode_action_auto_download">Auto download</string>

View file

@ -268,7 +268,7 @@
<string name="episode_action_chromecast_episode">Chromecastovat epizodu</string>
<string name="episode_action_chromecast_mirror">Chromecast jako zrcadlo</string>
<string name="episode_action_play_in_app">Přehrát v aplikace</string>
<string name="episode_action_play_in_vlc">Přehrát ve VLC</string>
<string name="episode_action_play_in_format">Přehrát ve %s</string>
<string name="episode_action_play_in_browser">Přehrát v prohlížeči</string>
<string name="episode_action_copy_link">Zkopírovat odkaz</string>
<string name="episode_action_auto_download">Automaticky stáhnout</string>

View file

@ -281,7 +281,7 @@
<string name="episode_action_chromecast_episode">Chromecast-Episode</string>
<string name="episode_action_chromecast_mirror">Chromecastmirror</string>
<string name="episode_action_play_in_app">In App wiedergeben</string>
<string name="episode_action_play_in_vlc">In VLC wiedergeben</string>
<string name="episode_action_play_in_format">In %s wiedergeben</string>
<string name="episode_action_play_in_browser">In Browser wiedergeben</string>
<string name="episode_action_copy_link">Link kopieren</string>
<string name="episode_action_auto_download">Auto Download</string>

View file

@ -156,7 +156,7 @@
<item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</item>
<item>@string/episode_action_play_in_vlc</item>
<item>@string/episode_action_play_in_format</item>
<item>@string/episode_action_play_in_browser</item>
<item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item>

View file

@ -269,7 +269,7 @@
<string name="episode_action_chromecast_episode">Episodio Chromecast</string>
<string name="episode_action_chromecast_mirror">Espejo Chromecast</string>
<string name="episode_action_play_in_app">Reproducir en la app</string>
<string name="episode_action_play_in_vlc">Reproducir en VLC</string>
<string name="episode_action_play_in_format">Reproducir en %s</string>
<string name="episode_action_play_in_browser">Reproducir en el navegador</string>
<string name="episode_action_copy_link">Copiar enlace</string>
<string name="episode_action_auto_download">Descarga automática</string>

View file

@ -164,7 +164,7 @@
<string name="episode_action_chromecast_episode">Episode Chromecast</string>
<string name="episode_action_chromecast_mirror">Miroir Chromecast</string>
<string name="episode_action_play_in_app">Lecture dans l\'application</string>
<string name="episode_action_play_in_vlc">Lecture dans VLC</string>
<string name="episode_action_play_in_format">Lecture dans %s</string>
<string name="episode_action_play_in_browser">Lecture dans le navigateur</string>
<string name="episode_action_copy_link">Copier le lien</string>
<string name="episode_action_auto_download">Téléchargement Automatique</string>

View file

@ -136,7 +136,7 @@
<string name="episode_action_chromecast_episode">क्रोमकास्ट एपिसोड</string>
<string name="episode_action_chromecast_mirror">कक्रोमकास्ट मिरर</string>
<string name="episode_action_play_in_app">एप्प मैं चलाये</string>
<string name="episode_action_play_in_vlc">VLC में चलाए</string>
<string name="episode_action_play_in_format">%s में चलाए</string>
<string name="episode_action_play_in_browser">Browser में चलाए</string>
<string name="episode_action_copy_link">लिंक कॉपी करें</string>
<string name="episode_action_auto_download">डाउनलोड करे</string>

View file

@ -299,7 +299,7 @@
<string name="episode_action_chromecast_episode">Chromecast epizoda</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
<string name="episode_action_play_in_app">Pokreni u aplikaciji</string>
<string name="episode_action_play_in_vlc">Pokreni u VLC-u</string>
<string name="episode_action_play_in_format">Pokreni u %s</string>
<string name="episode_action_play_in_browser">Pokreni u pregledniku</string>
<string name="episode_action_copy_link">Kopiraj poveznicu</string>
<string name="episode_action_auto_download">Automatsko preuzimanje</string>

View file

@ -264,7 +264,7 @@
<string name="episode_action_chromecast_episode">Episode Chromecast</string>
<string name="episode_action_chromecast_mirror">Mirror Chromecast</string>
<string name="episode_action_play_in_app">Putar di aplikasi</string>
<string name="episode_action_play_in_vlc">Putar di VLC</string>
<string name="episode_action_play_in_format">Putar di %s</string>
<string name="episode_action_play_in_browser">Putar di browser</string>
<string name="episode_action_copy_link">Salin tautan</string>
<string name="episode_action_auto_download">Download otomatis</string>

View file

@ -271,7 +271,7 @@
<string name="episode_action_chromecast_episode">Chromecast</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
<string name="episode_action_play_in_app">Riproduci in app</string>
<string name="episode_action_play_in_vlc">Riproduci in VLC</string>
<string name="episode_action_play_in_format">Riproduci in %s</string>
<string name="episode_action_play_in_browser">Riproduci nel browser</string>
<string name="episode_action_copy_link">Copia link</string>
<string name="episode_action_auto_download">Download</string>

View file

@ -190,7 +190,7 @@
<string name="episode_action_chromecast_episode">Епизода на Chromecast</string>
<string name="episode_action_chromecast_mirror">Огледало на Chromecastr</string>
<string name="episode_action_play_in_app">Пушти во апликацијата</string>
<string name="episode_action_play_in_vlc">Пушти на VLC</string>
<string name="episode_action_play_in_format">Пушти на %s</string>
<string name="episode_action_play_in_browser">Пушти на прелистувач</string>
<string name="episode_action_copy_link">Копирај линк</string>
<string name="episode_action_auto_download">Авто превземање</string>

View file

@ -175,7 +175,7 @@
<!-- <string name="episode_action_chomecast_episode">Chromecast Episode</string>
<string name="episode_action_chomecast_mirror">Chromecast Mirror</string> -->
<string name="episode_action_play_in_app">ആപ്പിൽ പ്ലേയ് ചെയ്യുക</string>
<string name="episode_action_play_in_vlc">VLCയിൽ പ്ലേയ് ചെയ്യുക</string>
<string name="episode_action_play_in_format">%sയിൽ പ്ലേയ് ചെയ്യുക</string>
<string name="episode_action_play_in_browser">ബ്രൗസറിൽ പ്ലേയ് ചെയ്യുക</string>
<string name="episode_action_copy_link">ലിങ്ക് പകർത്തുക</string>
<string name="episode_action_auto_download">ഡൌൺലോഡ് ചെയ്യൂ</string>

View file

@ -145,7 +145,7 @@
<string name="episode_action_chromecast_episode">aauugghhooo-ahah ohaaauugghh</string>
<string name="episode_action_chromecast_mirror">aoohaaahhu ahouuhhh</string>
<string name="episode_action_play_in_app">ooo-ahahaauuh aaahhu</string>
<string name="episode_action_play_in_vlc">ooo-ahah ohaauuh</string>
<string name="episode_action_play_in_format">ooo-ahah ohaauuh</string>
<string name="episode_action_play_in_browser">ahoha ooo-ahahohoohah oooohh</string>
<string name="episode_action_copy_link">aauugghhahhaauugghh</string>
<string name="episode_action_auto_download">aaaghhoooohh aaahhu ahooo</string>

View file

@ -274,7 +274,7 @@
<string name="episode_action_chromecast_episode">Chromecast aflevering</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
<string name="episode_action_play_in_app">Speel in app</string>
<string name="episode_action_play_in_vlc">Speel in VLC</string>
<string name="episode_action_play_in_format">Speel in %s</string>
<string name="episode_action_play_in_browser">Speel in browser</string>
<string name="episode_action_copy_link">Kopieer link</string>
<string name="episode_action_auto_download">Automatisch downloaden</string>

View file

@ -196,7 +196,7 @@
<string name="episode_action_chromecast_episode">Støpt Episode</string>
<string name="episode_action_chromecast_mirror">Støpt Speil</string>
<string name="episode_action_play_in_app">Spill i appen</string>
<string name="episode_action_play_in_vlc">Spill i VLC</string>
<string name="episode_action_play_in_format">Spill i %s</string>
<string name="episode_action_play_in_browser">Spill i nettleseren</string>
<string name="episode_action_copy_link">Kopier link</string>
<string name="episode_action_auto_download">Automatisk nedlasting</string>

View file

@ -165,7 +165,7 @@
<item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</item>
<item>@string/episode_action_play_in_vlc</item>
<item>@string/episode_action_play_in_format</item>
<item>@string/episode_action_play_in_browser</item>
<item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item>

View file

@ -252,7 +252,7 @@
<string name="episode_action_chromecast_episode">Chromecast odcinka</string>
<string name="episode_action_chromecast_mirror">Chromecast mirroru</string>
<string name="episode_action_play_in_app">Odtwórz w aplikacji</string>
<string name="episode_action_play_in_vlc">Odtwórz w VLC</string>
<string name="episode_action_play_in_format">Odtwórz w %s</string>
<string name="episode_action_play_in_browser">Odtwórz w przeglądarce</string>
<string name="episode_action_copy_link">Kopiuj link</string>
<string name="episode_action_auto_download">Automatyczne pobieranie</string>

View file

@ -268,7 +268,7 @@
<string name="episode_action_chromecast_episode">Episódio pelo Chromecast</string>
<string name="episode_action_chromecast_mirror">Alternativa pelo Chromecast</string>
<string name="episode_action_play_in_app">Reproduzir na app</string>
<string name="episode_action_play_in_vlc">Reproduzir no VLC</string>
<string name="episode_action_play_in_format">Reproduzir no %s</string>
<string name="episode_action_play_in_browser">Reproduzir no navegador</string>
<string name="episode_action_copy_link">Copiar link</string>
<string name="episode_action_auto_download">Transferência Automática</string>

View file

@ -267,7 +267,7 @@
<string name="episode_action_chromecast_episode">Chromecast</string>
<string name="episode_action_chromecast_mirror">Chromecast alternativ</string>
<string name="episode_action_play_in_app">Redă în Aplicație</string>
<string name="episode_action_play_in_vlc">Redă în VLC</string>
<string name="episode_action_play_in_format">Redă în %s</string>
<string name="episode_action_play_in_browser">Redă în Browser</string>
<string name="episode_action_copy_link">Copiază link-ul</string>
<string name="episode_action_auto_download">Auto-descărcare</string>

View file

@ -167,7 +167,7 @@
<string name="episode_action_chromecast_episode">Chromecasta ett Avsnitt</string>
<string name="episode_action_chromecast_mirror">Chromecasta en Länk</string>
<string name="episode_action_play_in_app">Spela upp i appen</string>
<string name="episode_action_play_in_vlc">Spela upp i VLC</string>
<string name="episode_action_play_in_format">Spela upp i %s</string>
<string name="episode_action_play_in_browser">Spela upp i webbläsaren</string>
<string name="episode_action_copy_link">Kopiera länk</string>
<string name="episode_action_auto_download">Automatisk nerladdning</string>

View file

@ -204,7 +204,7 @@
<string name="episode_action_chromecast_episode">Chromecast Episode</string>
<string name="episode_action_chromecast_mirror">Chromecast Mirror</string>
<string name="episode_action_play_in_app">I-play sa App</string>
<string name="episode_action_play_in_vlc">I-play sa VLC</string>
<string name="episode_action_play_in_format">I-play sa %s</string>
<string name="episode_action_play_in_browser">I-play sa browser</string>
<string name="episode_action_copy_link">Kopyahin ang Link</string>
<string name="episode_action_auto_download">Awtomatiking i-download</string>

View file

@ -156,7 +156,7 @@
<item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</item>
<item>@string/episode_action_play_in_vlc</item>
<item>@string/episode_action_play_in_format</item>
<item>@string/episode_action_play_in_browser</item>
<item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item>

View file

@ -272,7 +272,7 @@
<string name="episode_action_chromecast_episode">Bölümü Chromecast ile yayınla</string>
<string name="episode_action_chromecast_mirror">Bağlantıyı Chromecast ile yayınla</string>
<string name="episode_action_play_in_app">Uygulamada oynat</string>
<string name="episode_action_play_in_vlc">VLC\'de oynat</string>
<string name="episode_action_play_in_format">%s\'de oynat</string>
<string name="episode_action_play_in_browser">Tarayıcıda oynat</string>
<string name="episode_action_copy_link">Linki kopyala</string>
<string name="episode_action_auto_download">Otomatik indir</string>

View file

@ -157,7 +157,7 @@
<item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</item>
<item>@string/episode_action_play_in_vlc</item>
<item>@string/episode_action_play_in_format</item>
<item>@string/episode_action_play_in_browser</item>
<item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item>

View file

@ -292,7 +292,7 @@
<string name="episode_action_chromecast_episode">Tập Chromecast</string>
<string name="episode_action_chromecast_mirror">Chiếu Chromecast</string>
<string name="episode_action_play_in_app">Xem với trình phát mặc định</string>
<string name="episode_action_play_in_vlc">Xem với trình phát VLC</string>
<string name="episode_action_play_in_format">Xem với trình phát %s</string>
<string name="episode_action_play_in_browser">Xem tại trình duyệt</string>
<string name="episode_action_copy_link">Sao chép liên kết</string>
<string name="episode_action_auto_download">Tự động tải xuống</string>

View file

@ -303,7 +303,7 @@
<string name="episode_action_chromecast_episode">投屏剧集</string>
<string name="episode_action_chromecast_mirror">投屏镜像</string>
<string name="episode_action_play_in_app">在应用中播放</string>
<string name="episode_action_play_in_vlc">在 VLC 中播放</string>
<string name="episode_action_play_in_format">在 %s 中播放</string>
<string name="episode_action_play_in_browser">在浏览器中播放</string>
<string name="episode_action_copy_link">复制链接</string>
<string name="episode_action_auto_download">自动下载</string>

View file

@ -33,6 +33,22 @@
<item>6</item>
</array>
<array name="player_pref_names">
<item>@string/player_settings_play_in_app</item>
<item>@string/player_settings_play_in_vlc</item>
<item>@string/player_settings_play_in_mpv</item>
<item>@string/player_settings_play_in_web</item>
<item>@string/player_settings_play_in_browser</item>
</array>
<array name="player_pref_values">
<item>1</item>
<item>2</item>
<item>5</item>
<item>4</item>
<item>3</item>
</array>
<array name="limit_title_rez_pref_names">
<item>@string/resolution_and_title</item>
<item>@string/title</item>
@ -175,7 +191,7 @@
<item>@string/episode_action_chromecast_episode</item>
<item>@string/episode_action_chromecast_mirror</item>
<item>@string/episode_action_play_in_app</item>
<item>@string/episode_action_play_in_vlc</item>
<item>@string/episode_action_play_in_format</item>
<item>@string/episode_action_play_in_browser</item>
<item>@string/episode_action_copy_link</item>
<item>@string/episode_action_auto_download</item>

View file

@ -14,6 +14,7 @@
<string name="subtitle_settings_key" translatable="false">subtitle_settings_key</string>
<string name="subtitle_settings_chromecast_key" translatable="false">subtitle_settings_chromecast_key</string>
<string name="quality_pref_key" translatable="false">quality_pref_key</string>
<string name="player_pref_key" translatable="false">player_pref_key</string>
<string name="prefer_limit_title_key" translatable="false">prefer_limit_title_key</string>
<string name="prefer_limit_title_rez_key" translatable="false">prefer_limit_title_rez_key</string>
<string name="video_buffer_size_key" translatable="false">video_buffer_size_key</string>
@ -368,7 +369,7 @@
<string name="episode_action_chromecast_episode">Chromecast episode</string>
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
<string name="episode_action_play_in_app">Play in app</string>
<string name="episode_action_play_in_vlc">Play in VLC</string>
<string name="episode_action_play_in_format">Play in %s</string>
<string name="episode_action_play_in_browser">Play in browser</string>
<string name="episode_action_copy_link">Copy link</string>
<string name="episode_action_auto_download">Auto download</string>
@ -634,4 +635,12 @@
<string name="extension_install_first">Install the extension first</string>
<string name="hls_playlist">HLS Playlist</string>
<string name="player_pref">Preferred video player</string>
<string name="player_settings_play_in_app">Internal player</string>
<string name="player_settings_play_in_vlc">VLC</string>
<string name="player_settings_play_in_mpv">MPV</string>
<string name="player_settings_play_in_web">Web Video Cast</string>
<string name="player_settings_play_in_browser">Browser</string>
<string name="app_not_found_error">App not found</string>
</resources>

View file

@ -18,6 +18,11 @@
android:title="@string/watch_quality_pref"
android:icon="@drawable/ic_baseline_hd_24" />
<Preference
android:key="@string/player_pref_key"
android:title="@string/player_pref"
android:icon="@drawable/netflix_play" />
<Preference
android:key="@string/prefer_limit_title_key"
android:title="@string/limit_title"