subtitle download and other sub settings

This commit is contained in:
LagradOst 2021-08-22 19:14:48 +02:00
parent 440a9810be
commit 10f25eaefe
11 changed files with 547 additions and 229 deletions

View file

@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.PlayerFragment
import com.lagradost.cloudstream3.ui.player.UriData
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -69,7 +70,10 @@ object DownloadButtonSetup {
val info =
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id)
?: return
val keyInfo = act.getKey<VideoDownloadManager.DownloadedFileInfo>(
VideoDownloadManager.KEY_DOWNLOAD_INFO,
click.data.id.toString()
) ?: return
(act as FragmentActivity).supportFragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.enter_anim,
@ -82,6 +86,8 @@ object DownloadButtonSetup {
PlayerFragment.newInstance(
UriData(
info.path.toString(),
keyInfo.relativePath,
keyInfo.displayName,
click.data.id,
headerName ?: "null",
if (click.data.episode <= 0) null else click.data.episode,

View file

@ -66,6 +66,7 @@ import com.lagradost.cloudstream3.ui.result.ResultViewModel
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle
import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest
import com.lagradost.cloudstream3.utils.AppUtils.getVideoContentUri
@ -78,6 +79,7 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
@ -86,7 +88,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.VIDEO_PLAYER_BRIGHTNESS
import com.lagradost.cloudstream3.utils.getId
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getId
import kotlinx.android.synthetic.main.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.coroutines.*
@ -140,6 +143,8 @@ data class PlayerData(
data class UriData(
val uri: String,
val relativePath: String,
val displayName: String,
val id: Int?,
val name: String,
val episode: Int?,
@ -280,7 +285,8 @@ class PlayerFragment : Fragment() {
fadeAnimation.fillAfter = true
subView?.let { sView ->
val move = if (isShowing) -((bottom_player_bar?.height?.toFloat() ?: 0f) + 10.toPx) else -subStyle.elevation.toPx.toFloat()
val move = if (isShowing) -((bottom_player_bar?.height?.toFloat()
?: 0f) + 10.toPx) else -subStyle.elevation.toPx.toFloat()
ObjectAnimator.ofFloat(sView, "translationY", move).apply {
duration = 200
start()
@ -810,13 +816,40 @@ class PlayerFragment : Fragment() {
}
}
private fun setPreferredSubLanguage(lang: String?) {
//val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener
val realLang = if (lang.isNullOrBlank()) "" else lang
preferredSubtitles =
if (realLang.length == 2) SubtitleHelper.fromTwoLettersToLanguage(realLang) ?: realLang else realLang
if (!this::exoPlayer.isInitialized) return
(exoPlayer?.trackSelector as DefaultTrackSelector?)?.let { trackSelector ->
if (lang.isNullOrBlank()) {
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setPreferredTextLanguage(realLang)
//.setRendererDisabled(textRendererIndex, true)
)
} else {
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setPreferredTextLanguage(realLang)
//.setRendererDisabled(textRendererIndex, false)
)
}
}
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { ctx ->
setPreferredSubLanguage(ctx.getAutoSelectLanguageISO639_1())
}
subView = player_view.findViewById(R.id.exo_subtitles)
subView?.let { sView ->
(sView.parent as ViewGroup?) ?.removeView(sView)
(sView.parent as ViewGroup?)?.removeView(sView)
subtitle_holder.addView(sView)
}
@ -860,7 +893,7 @@ class PlayerFragment : Fragment() {
epData.index,
episodes,
links,
getSubs() ?: ArrayList(),
context?.getSubs(supportsDownloadedFiles = false) ?: emptyList(),
index,
exoPlayer.currentPosition
)
@ -929,7 +962,12 @@ class PlayerFragment : Fragment() {
}
sources_btt.visibility =
if (isDownloadedFile) GONE else VISIBLE
if (isDownloadedFile)
if (context?.getSubs()?.isNullOrEmpty() != false)
GONE else VISIBLE
else VISIBLE
player_media_route_button.visibility =
if (isDownloadedFile) GONE else VISIBLE
if (savedInstanceState != null) {
@ -1154,9 +1192,6 @@ class PlayerFragment : Fragment() {
}
sources_btt.setOnClickListener {
lateinit var dialog: AlertDialog
getUrls()?.let { it1 ->
sortUrls(it1).let { sources ->
val isPlaying = exoPlayer.isPlaying
exoPlayer.pause()
val currentSubtitles = activeSubtitles
@ -1177,11 +1212,38 @@ class PlayerFragment : Fragment() {
SubtitlesFragment.push(activity)
sourceDialog.dismiss()
}
var sourceIndex = 0
var startSource = 0
var sources: List<ExtractorLink> = emptyList()
val startSource = sources.indexOf(getCurrentUrl())
var sourceIndex = startSource
val startSubtitle = currentSubtitles.indexOf(preferredSubtitles) + 1
var subtitleIndex = startSubtitle
val nonSortedUrls = getUrls()
if (nonSortedUrls.isNullOrEmpty()) {
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.visibility = GONE
} else {
sources = sortUrls(nonSortedUrls)
startSource = sources.indexOf(getCurrentUrl())
sourceIndex = startSource
val sourcesArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
sourcesArrayAdapter.addAll(sources.map { it.name })
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
providerList.adapter = sourcesArrayAdapter
providerList.setSelection(sourceIndex)
providerList.setItemChecked(sourceIndex, true)
providerList.setOnItemClickListener { _, _, which, _ ->
sourceIndex = which
providerList.setItemChecked(which, true)
}
sourceDialog.setOnDismissListener {
activity?.hideSystemUI()
}
}
val startIndexFromMap = currentSubtitles.map { it.removeSuffix(" ") }.indexOf(preferredSubtitles.removeSuffix(" ")) + 1
var subtitleIndex = startIndexFromMap
if (currentSubtitles.isEmpty()) {
sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
@ -1202,23 +1264,6 @@ class PlayerFragment : Fragment() {
}
}
val sourcesArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
sourcesArrayAdapter.addAll(sources.map { it.name })
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
providerList.adapter = sourcesArrayAdapter
providerList.setSelection(sourceIndex)
providerList.setItemChecked(sourceIndex, true)
providerList.setOnItemClickListener { _, _, which, _ ->
sourceIndex = which
providerList.setItemChecked(which, true)
}
sourceDialog.setOnDismissListener {
activity?.hideSystemUI()
}
cancelButton.setOnClickListener {
sourceDialog.dismiss()
}
@ -1234,57 +1279,11 @@ class PlayerFragment : Fragment() {
}
}
if (subtitleIndex != startSubtitle) {
val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener
(exoPlayer.trackSelector as DefaultTrackSelector?)?.let { trackSelector ->
if (subtitleIndex <= 0) {
preferredSubtitles = ""
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setPreferredTextLanguage("")
.setRendererDisabled(textRendererIndex, true)
)
} else {
val currentPreferredSub = currentSubtitles[subtitleIndex - 1]
preferredSubtitles = currentPreferredSub
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setPreferredTextLanguage(currentPreferredSub)
.setRendererDisabled(textRendererIndex, false)
)
}
}
if (subtitleIndex != startIndexFromMap) {
setPreferredSubLanguage(if (subtitleIndex <= 0) null else currentSubtitles[subtitleIndex - 1])
}
sourceDialog.dismiss()
}
/*
*/
/*
val sourcesText = sources.map { it.name }
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setTitle("Pick source")
builder.setOnDismissListener {
activity?.hideSystemUI()
}
builder.setSingleChoiceItems(
sourcesText.toTypedArray(),
sources.indexOf(getCurrentUrl())
) { _, which ->
//val speed = speedsText[which]
//Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show()
playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0
setMirrorId(sources[which].getId())
initPlayer(getCurrentUrl())
dialog.dismiss()
activity?.hideSystemUI()
}
dialog = builder.create()
dialog.show()*/
}
}
}
player_view.resizeMode = resizeModes[resizeMode]
@ -1347,9 +1346,25 @@ class PlayerFragment : Fragment() {
}
}
private fun getSubs(): List<SubtitleFile>? {
private fun Context.getSubs(supportsDownloadedFiles: Boolean = true): List<SubtitleFile>? {
return try {
if (isDownloadedFile) {
if (!supportsDownloadedFiles) return null
val list = ArrayList<SubtitleFile>()
VideoDownloadManager.getFolder(this, uriData.relativePath)?.forEach { file ->
val name = uriData.displayName.removeSuffix(".mp4")
if (file.first != uriData.displayName && file.first.startsWith(name)) {
val realName = file.first.removePrefix(name)
.removeSuffix(".vtt")
.removeSuffix(".srt")
.removeSuffix(".txt")
list.add(SubtitleFile(realName.ifBlank { "Default" }, file.second.toString()))
}
}
return list
} else {
allEpisodesSubs[getEpisode()?.id]
}
} catch (e: Exception) {
null
}
@ -1599,8 +1614,7 @@ class PlayerFragment : Fragment() {
}
}
val subs = getSubs()
if (subs != null) {
val subs = context?.getSubs() ?: emptyList()
val subItems = ArrayList<MediaItem.Subtitle>()
val subItemsId = ArrayList<String>()
@ -1619,7 +1633,6 @@ class PlayerFragment : Fragment() {
activeSubtitles = subItemsId
mediaItemBuilder.setSubtitles(subItems)
}
//might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps
@ -1825,6 +1838,16 @@ class PlayerFragment : Fragment() {
})
} catch (e: java.lang.IllegalStateException) {
println("Warning: Illegal state exception in PlayerFragment")
} finally {
setPreferredSubLanguage(
if(isDownloadedFile) {
if(activeSubtitles.isNotEmpty()) {
activeSubtitles.first()
} else null
} else {
preferredSubtitles
}
)
}
}

View file

@ -24,9 +24,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.request.RequestOptions.bitmapTransform
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
@ -52,6 +49,8 @@ import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.player.PlayerData
import com.lagradost.cloudstream3.ui.player.PlayerFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
@ -64,13 +63,11 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
@ -239,7 +236,7 @@ class ResultFragment : Fragment() {
var startAction: Int? = null
private fun lateFixDownloadButton(show: Boolean) {
if(!show || currentType?.isMovieType() == false) {
if (!show || currentType?.isMovieType() == false) {
result_movie_parent.visibility = GONE
result_episodes_text.visibility = VISIBLE
result_episodes.visibility = VISIBLE
@ -381,7 +378,11 @@ class ResultFragment : Fragment() {
return false
}
fun aquireSingeExtractorLink(links: List<ExtractorLink>, title: String, callback: (ExtractorLink) -> Unit) {
fun acquireSingeExtractorLink(
links: List<ExtractorLink>,
title: String,
callback: (ExtractorLink) -> Unit
) {
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setTitle(title)
@ -392,8 +393,8 @@ class ResultFragment : Fragment() {
builder.create().show()
}
fun aquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
aquireSingeExtractorLink(currentLinks ?: return, title, callback)
fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
acquireSingeExtractorLink(currentLinks ?: return, title, callback)
}
fun startChromecast(startIndex: Int) {
@ -412,7 +413,7 @@ class ResultFragment : Fragment() {
)
}
fun startDownload(links: List<ExtractorLink>) {
fun startDownload(links: List<ExtractorLink>, subs: List<SubtitleFile>?) {
val isMovie = currentIsMovie ?: return
val titleName = sanitizeFilename(currentHeaderName ?: return)
@ -481,6 +482,36 @@ class ResultFragment : Fragment() {
meta,
links
)
// 1. Checks if the lang should be downloaded
// 2. Makes it into the download format
// 3. Downloads it as a .vtt file
val downloadList = ctx.getDownloadSubsLanguageISO639_1()
main {
subs?.let { subsList ->
subsList.filter { downloadList.contains(SubtitleHelper.fromLanguageToTwoLetters(it.lang)) }
.map { ExtractorSubtitleLink(it.lang, it.url, "") }
.forEach { link ->
val epName = meta.name ?: "Episode ${meta.episode}"
val fileName =
sanitizeFilename(epName + if (downloadList.size > 1) " ${link.name}" else "")
val topFolder = "$folder"
withContext(Dispatchers.IO) {
VideoDownloadManager.downloadThing(
ctx,
link,
fileName,
topFolder,
"vtt",
false,
null
) {
// no notification
}
}
}
}
}
}
}
@ -547,10 +578,10 @@ class ResultFragment : Fragment() {
dialog.show()
}
ACTION_COPY_LINK -> {
aquireSingeExtractorLink("Copy Link") { link ->
acquireSingeExtractorLink("Copy Link") { link ->
val serviceClipboard =
(requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?)
?: return@aquireSingeExtractorLink
?: return@acquireSingeExtractorLink
val clip = ClipData.newPlainText(link.name, link.url)
serviceClipboard.setPrimaryClip(clip)
Toast.makeText(requireContext(), "Text Copied", Toast.LENGTH_SHORT).show()
@ -558,7 +589,7 @@ class ResultFragment : Fragment() {
}
ACTION_PLAY_EPISODE_IN_BROWSER -> {
aquireSingeExtractorLink("Play in Browser") { link ->
acquireSingeExtractorLink("Play in Browser") { link ->
val i = Intent(ACTION_VIEW)
i.data = Uri.parse(link.url)
startActivity(i)
@ -566,7 +597,7 @@ class ResultFragment : Fragment() {
}
ACTION_CHROME_CAST_MIRROR -> {
aquireSingeExtractorLink("Cast Mirror") { link ->
acquireSingeExtractorLink("Cast Mirror") { link ->
val mirrorIndex = currentLinks?.indexOf(link) ?: -1
startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex)
}
@ -657,15 +688,15 @@ class ResultFragment : Fragment() {
}
ACTION_DOWNLOAD_EPISODE -> {
startDownload(currentLinks ?: return@main)
startDownload(currentLinks ?: return@main, currentSubs)
}
ACTION_DOWNLOAD_MIRROR -> {
aquireSingeExtractorLink(
acquireSingeExtractorLink(
(currentLinks ?: return@main).filter { !it.isM3u8 },
"Download Mirror"
) { link ->
startDownload(listOf(link))
startDownload(listOf(link), currentSubs)
}
}
}
@ -704,7 +735,7 @@ class ResultFragment : Fragment() {
}
observe(viewModel.episodes) { episodeList ->
lateFixDownloadButton( episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
when (startAction) {
START_ACTION_RESUME_LATEST -> {

View file

@ -26,12 +26,17 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.subtitle_settings.*
const val SUBTITLE_KEY = "subtitle_settings"
const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select"
const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download"
data class SaveCaptionStyle(
var foregroundColor: Int,
@ -111,6 +116,14 @@ class SubtitlesFragment : Fragment() {
val metrics: DisplayMetrics = Resources.getSystem().displayMetrics
return TypedValue.applyDimension(unit, size, metrics).toInt()
}
fun Context.getDownloadSubsLanguageISO639_1(): List<String> {
return getKey(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en")
}
fun Context.getAutoSelectLanguageISO639_1(): String {
return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en"
}
}
private fun onColorSelected(stuff: Pair<Int, Int>) {
@ -296,10 +309,57 @@ class SubtitlesFragment : Fragment() {
}
}
subs_font.setOnLongClickListener {
subs_font.setOnLongClickListener { textView ->
state.typeface = null
it.context.updateState()
Toast.makeText(it.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
textView.context.updateState()
Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
return@setOnLongClickListener true
}
subs_auto_select_language.setOnClickListener { textView ->
val langMap = arrayListOf(
SubtitleHelper.Language639("None", "None", "", "", "", "", ""),
)
langMap.addAll(SubtitleHelper.languages)
val lang639_1 = langMap.map { it.ISO_639_1 }
textView.context.showDialog(
langMap.map { it.languageName },
lang639_1.indexOf(textView.context.getAutoSelectLanguageISO639_1()),
(textView as TextView).text.toString(),
true,
dismissCallback
) { index ->
textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
}
}
subs_auto_select_language.setOnLongClickListener { textView ->
textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
return@setOnLongClickListener true
}
subs_download_languages.setOnClickListener { textView ->
val langMap = SubtitleHelper.languages
val lang639_1 = langMap.map { it.ISO_639_1 }
val keys = textView.context.getDownloadSubsLanguageISO639_1()
val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 }
textView.context.showMultiDialog(
langMap.map { it.languageName },
keyMap,
(textView as TextView).text.toString(),
dismissCallback
) { indexList ->
textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
}
}
subs_download_languages.setOnLongClickListener { textView ->
textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show()
return@setOnLongClickListener true
}

View file

@ -6,15 +6,17 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
data class ExtractorLink(
val source: String,
val name: String,
val url: String,
val referer: String,
override val url: String,
override val referer: String,
val quality: Int,
val isM3u8: Boolean = false,
)
) : VideoDownloadManager.IDownloadableMinimum
fun ExtractorLink.getId(): Int {
return url.hashCode()
}
data class ExtractorSubtitleLink(
val name: String,
override val url: String,
override val referer: String,
) : VideoDownloadManager.IDownloadableMinimum
enum class Qualities(var value: Int) {
Unknown(0),

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.core.util.forEach
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
@ -15,20 +16,22 @@ object SingleSelectionHelper {
fun Context.showDialog(
dialog: Dialog,
items: List<String>,
selectedIndex: Int,
selectedIndex: List<Int>,
name: String,
showApply: Boolean,
callback: (Int) -> Unit,
isMultiSelect: Boolean,
callback: (List<Int>) -> Unit,
dismissCallback: () -> Unit
) {
val realShowApply = showApply || isMultiSelect
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
applyHolder.visibility = if (showApply) View.VISIBLE else View.GONE
if (!showApply) {
applyHolder.visibility = if (realShowApply) View.VISIBLE else View.GONE
if (!realShowApply) {
val params = listView.layoutParams as LinearLayout.LayoutParams
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
listView.layoutParams = params
@ -40,29 +43,45 @@ object SingleSelectionHelper {
arrayAdapter.addAll(items)
listView.adapter = arrayAdapter
if (isMultiSelect) {
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
} else {
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
}
listView.setSelection(selectedIndex)
listView.setItemChecked(selectedIndex, true)
for (select in selectedIndex) {
listView.setItemChecked(select, true)
}
var currentIndex = selectedIndex
selectedIndex.minOrNull()?.let {
listView.setSelection(it)
}
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
dialog.setOnDismissListener {
dismissCallback.invoke()
}
listView.setOnItemClickListener { _, _, which, _ ->
if (showApply) {
currentIndex = which
// lastSelectedIndex = which
if (realShowApply) {
if (!isMultiSelect) {
listView.setItemChecked(which, true)
}
} else {
callback.invoke(which)
callback.invoke(listOf(which))
dialog.dismiss()
}
}
if (showApply) {
if (realShowApply) {
applyButton.setOnClickListener {
callback.invoke(currentIndex)
val list = ArrayList<Int>()
for (index in 0 until listView.count) {
if (listView.checkedItemPositions[index])
list.add(index)
}
callback.invoke(list)
dialog.dismiss()
}
cancelButton.setOnClickListener {
@ -71,6 +90,21 @@ object SingleSelectionHelper {
}
}
fun Context.showMultiDialog(
items: List<String>,
selectedIndex: List<Int>,
name: String,
dismissCallback: () -> Unit,
callback: (List<Int>) -> Unit,
) {
val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom).setView(R.layout.bottom_selection_dialog)
val dialog = builder.create()
dialog.show()
showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback)
}
fun Context.showDialog(
items: List<String>,
selectedIndex: Int,
@ -84,7 +118,16 @@ object SingleSelectionHelper {
val dialog = builder.create()
dialog.show()
showDialog(dialog, items, selectedIndex, name, showApply, callback, dismissCallback)
showDialog(
dialog,
items,
listOf(selectedIndex),
name,
showApply,
false,
{ if (it.isNotEmpty()) callback.invoke(it.first()) },
dismissCallback
)
}
fun Context.showBottomDialog(
@ -100,6 +143,15 @@ object SingleSelectionHelper {
builder.setContentView(R.layout.bottom_selection_dialog)
builder.show()
showDialog(builder, items, selectedIndex, name, showApply, callback, dismissCallback)
showDialog(
builder,
items,
listOf(selectedIndex),
name,
showApply,
false,
{ callback.invoke(it.first()) },
dismissCallback
)
}
}

View file

@ -207,7 +207,7 @@ object SubtitleHelper {
Language639("Malayalam", "മലയാളം", "ml", "mal", "mal", "mal", ""),
Language639("Maltese", "Malti", "mt", "mlt", "mlt", "mlt", ""),
Language639("Māori", "te reo Māori", "mi", "mri", "", "mri", ""),
Language639("Marathi (Marāṭhī)", "मराठी", "mr", "mar", "mar", "mar", ""),
Language639("Marathi", "मराठी", "mr", "mar", "mar", "mar", ""),
Language639("Marshallese", "Kajin M̧ajeļ", "mh", "mah", "mah", "mah", ""),
Language639("Mongolian", "Монгол хэл", "mn", "mon", "mon", "mon", ""),
Language639("Nauruan", "Dorerin Naoero", "na", "nau", "nau", "nau", ""),
@ -238,7 +238,7 @@ object SubtitleHelper {
Language639("Reunion Creole", "Kréol Rénioné", "rc", "rcf", "rcf", "rcf", ""),
Language639("Romanian", "limba română", "ro", "ron", "", "ron", ""),
Language639("Russian", "Русский", "ru", "rus", "rus", "rus", ""),
Language639("Sanskrit (Saṁskṛta)", "संस्कृतम्", "sa", "san", "san", "san", ""),
Language639("Sanskrit", "संस्कृतम्", "sa", "san", "san", "san", ""),
Language639("Sardinian", "sardu", "sc", "srd", "srd", "srd", ""),
Language639("Sindhi", "सिन्धी, سنڌي، سندھی‎", "sd", "snd", "snd", "snd", ""),
Language639("Northern Sami", "Davvisámegiella", "se", "sme", "sme", "sme", ""),

View file

@ -10,6 +10,7 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
@ -27,6 +28,7 @@ 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.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getExistingDownloadUriOrNullQ
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
@ -35,6 +37,7 @@ import java.lang.Thread.sleep
import java.net.URL
import java.net.URLConnection
import java.util.*
import kotlin.collections.ArrayList
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
const val DOWNLOAD_CHANNEL_NAME = "Downloads"
@ -85,6 +88,15 @@ object VideoDownloadManager {
Stop,
}
interface IDownloadableMinimum {
val url: String
val referer: String
}
fun VideoDownloadManager.IDownloadableMinimum.getId(): Int {
return url.hashCode()
}
data class DownloadEpisodeMetadata(
val id: Int,
val mainName: String,
@ -126,7 +138,7 @@ object VideoDownloadManager {
private const val SUCCESS_DOWNLOAD_DONE = 1
private const val SUCCESS_STOPPED = 2
private const val ERROR_DELETING_FILE = -1
private const val ERROR_DELETING_FILE = 3 // will not download the next one, but is still classified as an error
private const val ERROR_CREATE_FILE = -2
private const val ERROR_OPEN_FILE = -3
private const val ERROR_TOO_SMALL_CONNECTION = -4
@ -191,7 +203,7 @@ object VideoDownloadManager {
cachedBitmaps[url] = bitmap
}
return null
} catch (e : Exception) {
} catch (e: Exception) {
return null
}
}
@ -361,6 +373,69 @@ object VideoDownloadManager {
return tempName.replace(" ", " ").trim(' ')
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingFolderStartName(relativePath: String): List<Pair<String, Uri>>? {
try {
val projection = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only)
//MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only)
)
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath'"
val result = this.query(
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
projection, selection, null, null
)
val list = ArrayList<Pair<String, Uri>>()
result.use { c ->
if (c != null && c.count >= 1) {
c.moveToFirst()
while (true) {
val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val name = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
val uri = ContentUris.withAppendedId(
MediaStore.Downloads.EXTERNAL_CONTENT_URI, id
)
list.add(Pair(name, uri))
if (c.isLast) {
break
}
c.moveToNext()
}
/*
val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/
}
}
return list
} catch (e: Exception) {
return null
}
}
fun getFolder(context: Context, relativePath: String): List<Pair<String, Uri>>? {
if (isScopedStorage()) {
return context.contentResolver?.getExistingFolderStartName(relativePath)
} else {
val normalPath =
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace(
'/',
File.separatorChar
)
val folder = File(normalPath)
if (folder.isDirectory) {
return folder.listFiles().map { Pair(it.name, it.toUri()) }
}
return null
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
try {
@ -412,18 +487,24 @@ object VideoDownloadManager {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
private fun downloadSingleEpisode(
context: Context,
source: String?,
folder: String?,
ep: DownloadEpisodeMetadata,
link: ExtractorLink,
tryResume: Boolean = false,
): Int {
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
data class CreateNotificationMetadata(
val type: DownloadType,
val bytesDownloaded: Long,
val bytesTotal: Long,
)
fun downloadThing(
context: Context,
link: IDownloadableMinimum,
name: String,
folder: String?,
extension: String,
tryResume: Boolean,
parentId: Int?,
createNotificationCallback: (CreateNotificationMetadata) -> Unit
): Int {
val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
val displayName = "$name.mp4"
val displayName = "$name.$extension"
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
var resume = tryResume
@ -440,7 +521,9 @@ object VideoDownloadManager {
} else {
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
}
downloadDeleteEvent.invoke(ep.id)
parentId?.let {
downloadDeleteEvent.invoke(parentId)
}
return SUCCESS_STOPPED
}
@ -468,11 +551,18 @@ object VideoDownloadManager {
} else {
val contentUri =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val currentMimeType = when (extension) {
"vtt" -> "text/vtt"
"mp4" -> "video/mp4"
"srt" -> "text/plain"
else -> null
}
val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (currentMimeType != null)
put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
}
@ -539,9 +629,11 @@ object VideoDownloadManager {
}
val bytesTotal = contentLength + resumeLength
if (bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
context.setKey(KEY_DOWNLOAD_INFO, ep.id.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName))
parentId?.let {
context.setKey(KEY_DOWNLOAD_INFO, it.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName))
}
// Could use connection.contentType for mime types when creating the file,
// however file is already created and players don't go of file type
@ -573,15 +665,18 @@ object VideoDownloadManager {
else -> DownloadType.IsDownloading
}
parentId?.let { id ->
try {
downloadStatus[ep.id] = type
downloadStatusEvent.invoke(Pair(ep.id, type))
downloadProgressEvent.invoke(Triple(ep.id, bytesDownloaded, bytesTotal))
downloadStatus[id] = type
downloadStatusEvent.invoke(Pair(id, type))
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
} catch (e: Exception) {
// IDK MIGHT ERROR
}
}
createNotification(
createNotificationCallback.invoke(CreateNotificationMetadata(type, bytesDownloaded, bytesTotal))
/*createNotification(
context,
source,
link.name,
@ -589,12 +684,11 @@ object VideoDownloadManager {
type,
bytesDownloaded,
bytesTotal
)
)*/
}
val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
if (event.first == ep.id) {
if (event.first == parentId) {
when (event.second) {
DownloadActionType.Pause -> {
isPaused = true; updateNotification()
@ -611,6 +705,7 @@ object VideoDownloadManager {
}
}
if (parentId != null)
downloadEvent += downloadEventListener
// UPDATE DOWNLOAD NOTIFICATION
@ -625,7 +720,6 @@ object VideoDownloadManager {
}
}
val id = ep.id
// THE REAL READ
try {
while (true) {
@ -655,13 +749,16 @@ object VideoDownloadManager {
notificationCoroutine.cancel()
try {
if (parentId != null)
downloadEvent -= downloadEventListener
} catch (e: Exception) {
e.printStackTrace()
}
try {
downloadStatus.remove(ep.id)
parentId?.let {
downloadStatus.remove(it)
}
} catch (e: Exception) {
// IDK MIGHT ERROR
}
@ -669,15 +766,15 @@ object VideoDownloadManager {
// RETURN MESSAGE
return when {
isFailed -> {
downloadProgressEvent.invoke(Triple(id, 0, 0))
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
ERROR_CONNECTION_ERROR
}
isStopped -> {
downloadProgressEvent.invoke(Triple(id, 0, 0))
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
deleteFile()
}
else -> {
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) }
isDone = true
updateNotification()
SUCCESS_DOWNLOAD_DONE
@ -685,6 +782,29 @@ object VideoDownloadManager {
}
}
private fun downloadSingleEpisode(
context: Context,
source: String?,
folder: String?,
ep: DownloadEpisodeMetadata,
link: ExtractorLink,
tryResume: Boolean = false,
): Int {
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
return downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta ->
createNotification(
context,
source,
link.name,
ep,
meta.type,
meta.bytesDownloaded,
meta.bytesTotal
)
}
}
private fun downloadCheck(context: Context) {
if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
val pkg = downloadQueue.removeFirst()

View file

@ -14,6 +14,7 @@
android:baselineAligned="false">
<LinearLayout
android:id="@+id/sort_sources_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"

View file

@ -12,8 +12,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:textStyle="bold"
@ -78,6 +78,26 @@
android:text="@string/subs_subtitle_elevation"
style="@style/SettingsItem"
/>
<TextView
android:id="@+id/subs_auto_select_language"
android:text="@string/subs_auto_select_language"
style="@style/SettingsItem"
/>
<TextView
android:id="@+id/subs_download_languages"
android:text="@string/subs_download_languages"
style="@style/SettingsItem"
/>
<TextView
android:gravity="center"
android:text="@string/subs_hold_to_reset_to_default"
android:textSize="14sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content">
</TextView>
<LinearLayout
android:orientation="horizontal"

View file

@ -88,4 +88,7 @@
<string name="manual_check_update_key">manual_check_update</string>
<string name="prerelease_commit_hash">unknown_prerelease</string>
<string name="subs_auto_select_language">Auto Select Language</string>
<string name="subs_download_languages">Download Languages</string>
<string name="subs_hold_to_reset_to_default">Hold to reset to default</string>
</resources>