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.PlayerFragment
import com.lagradost.cloudstream3.ui.player.UriData import com.lagradost.cloudstream3.ui.player.UriData
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull 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.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -69,7 +70,10 @@ object DownloadButtonSetup {
val info = val info =
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id) VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id)
?: return ?: return
val keyInfo = act.getKey<VideoDownloadManager.DownloadedFileInfo>(
VideoDownloadManager.KEY_DOWNLOAD_INFO,
click.data.id.toString()
) ?: return
(act as FragmentActivity).supportFragmentManager.beginTransaction() (act as FragmentActivity).supportFragmentManager.beginTransaction()
.setCustomAnimations( .setCustomAnimations(
R.anim.enter_anim, R.anim.enter_anim,
@ -82,6 +86,8 @@ object DownloadButtonSetup {
PlayerFragment.newInstance( PlayerFragment.newInstance(
UriData( UriData(
info.path.toString(), info.path.toString(),
keyInfo.relativePath,
keyInfo.displayName,
click.data.id, click.data.id,
headerName ?: "null", headerName ?: "null",
if (click.data.episode <= 0) null else click.data.episode, 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.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle 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.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle
import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest
import com.lagradost.cloudstream3.utils.AppUtils.getVideoContentUri 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.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog 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.getNavigationBarHeight
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard 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.showSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.VIDEO_PLAYER_BRIGHTNESS 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.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.* import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -140,6 +143,8 @@ data class PlayerData(
data class UriData( data class UriData(
val uri: String, val uri: String,
val relativePath: String,
val displayName: String,
val id: Int?, val id: Int?,
val name: String, val name: String,
val episode: Int?, val episode: Int?,
@ -280,7 +285,8 @@ class PlayerFragment : Fragment() {
fadeAnimation.fillAfter = true fadeAnimation.fillAfter = true
subView?.let { sView -> 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 { ObjectAnimator.ofFloat(sView, "translationY", move).apply {
duration = 200 duration = 200
start() 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") @SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.let { ctx ->
setPreferredSubLanguage(ctx.getAutoSelectLanguageISO639_1())
}
subView = player_view.findViewById(R.id.exo_subtitles) subView = player_view.findViewById(R.id.exo_subtitles)
subView?.let { sView -> subView?.let { sView ->
(sView.parent as ViewGroup?) ?.removeView(sView) (sView.parent as ViewGroup?)?.removeView(sView)
subtitle_holder.addView(sView) subtitle_holder.addView(sView)
} }
@ -860,7 +893,7 @@ class PlayerFragment : Fragment() {
epData.index, epData.index,
episodes, episodes,
links, links,
getSubs() ?: ArrayList(), context?.getSubs(supportsDownloadedFiles = false) ?: emptyList(),
index, index,
exoPlayer.currentPosition exoPlayer.currentPosition
) )
@ -929,7 +962,12 @@ class PlayerFragment : Fragment() {
} }
sources_btt.visibility = 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 = player_media_route_button.visibility =
if (isDownloadedFile) GONE else VISIBLE if (isDownloadedFile) GONE else VISIBLE
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -1154,136 +1192,97 @@ class PlayerFragment : Fragment() {
} }
sources_btt.setOnClickListener { sources_btt.setOnClickListener {
lateinit var dialog: AlertDialog val isPlaying = exoPlayer.isPlaying
getUrls()?.let { it1 -> exoPlayer.pause()
sortUrls(it1).let { sources -> val currentSubtitles = activeSubtitles
val isPlaying = exoPlayer.isPlaying
exoPlayer.pause()
val currentSubtitles = activeSubtitles
val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
.setView(R.layout.player_select_source_and_subs) .setView(R.layout.player_select_source_and_subs)
val sourceDialog = sourceBuilder.create() val sourceDialog = sourceBuilder.create()
sourceDialog.show() sourceDialog.show()
// bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
val providerList = sourceDialog.findViewById<ListView>(R.id.sort_providers)!! val providerList = sourceDialog.findViewById<ListView>(R.id.sort_providers)!!
val subtitleList = sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!! val subtitleList = sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!!
val applyButton = sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!! val applyButton = sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!!
val cancelButton = sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!! val cancelButton = sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!!
val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!! val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!!
subsSettings.setOnClickListener { subsSettings.setOnClickListener {
SubtitlesFragment.push(activity) SubtitlesFragment.push(activity)
sourceDialog.dismiss() sourceDialog.dismiss()
} }
var sourceIndex = 0
var startSource = 0
var sources: List<ExtractorLink> = emptyList()
val startSource = sources.indexOf(getCurrentUrl()) val nonSortedUrls = getUrls()
var sourceIndex = startSource if (nonSortedUrls.isNullOrEmpty()) {
val startSubtitle = currentSubtitles.indexOf(preferredSubtitles) + 1 sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.visibility = GONE
var subtitleIndex = startSubtitle } else {
sources = sortUrls(nonSortedUrls)
startSource = sources.indexOf(getCurrentUrl())
sourceIndex = startSource
if (currentSubtitles.isEmpty()) { val sourcesArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE sourcesArrayAdapter.addAll(sources.map { it.name })
} else {
val subsArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
subsArrayAdapter.add("No Subtitles")
subsArrayAdapter.addAll(currentSubtitles)
subtitleList.adapter = subsArrayAdapter providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE providerList.adapter = sourcesArrayAdapter
providerList.setSelection(sourceIndex)
providerList.setItemChecked(sourceIndex, true)
subtitleList.setSelection(subtitleIndex) providerList.setOnItemClickListener { _, _, which, _ ->
subtitleList.setItemChecked(subtitleIndex, true) sourceIndex = which
providerList.setItemChecked(which, true)
subtitleList.setOnItemClickListener { _, _, which, _ ->
subtitleIndex = which
subtitleList.setItemChecked(which, true)
}
}
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()
}
applyButton.setOnClickListener {
if (sourceIndex != startSource) {
playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0
setMirrorId(sources[sourceIndex].getId())
initPlayer(getCurrentUrl())
} else {
if (isPlaying) {
// exoPlayer.play()
}
}
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)
)
}
}
}
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()*/
} }
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
} else {
val subsArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
subsArrayAdapter.add("No Subtitles")
subsArrayAdapter.addAll(currentSubtitles)
subtitleList.adapter = subsArrayAdapter
subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
subtitleList.setSelection(subtitleIndex)
subtitleList.setItemChecked(subtitleIndex, true)
subtitleList.setOnItemClickListener { _, _, which, _ ->
subtitleIndex = which
subtitleList.setItemChecked(which, true)
}
}
cancelButton.setOnClickListener {
sourceDialog.dismiss()
}
applyButton.setOnClickListener {
if (sourceIndex != startSource) {
playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0
setMirrorId(sources[sourceIndex].getId())
initPlayer(getCurrentUrl())
} else {
if (isPlaying) {
// exoPlayer.play()
}
}
if (subtitleIndex != startIndexFromMap) {
setPreferredSubLanguage(if (subtitleIndex <= 0) null else currentSubtitles[subtitleIndex - 1])
}
sourceDialog.dismiss()
} }
} }
@ -1347,9 +1346,25 @@ class PlayerFragment : Fragment() {
} }
} }
private fun getSubs(): List<SubtitleFile>? { private fun Context.getSubs(supportsDownloadedFiles: Boolean = true): List<SubtitleFile>? {
return try { return try {
allEpisodesSubs[getEpisode()?.id] 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) { } catch (e: Exception) {
null null
} }
@ -1599,28 +1614,26 @@ class PlayerFragment : Fragment() {
} }
} }
val subs = getSubs() val subs = context?.getSubs() ?: emptyList()
if (subs != null) { val subItems = ArrayList<MediaItem.Subtitle>()
val subItems = ArrayList<MediaItem.Subtitle>() val subItemsId = ArrayList<String>()
val subItemsId = ArrayList<String>()
for (sub in sortSubs(subs)) { for (sub in sortSubs(subs)) {
val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang
subItemsId.add(langId) subItemsId.add(langId)
subItems.add( subItems.add(
MediaItem.Subtitle( MediaItem.Subtitle(
Uri.parse(sub.url), Uri.parse(sub.url),
sub.url.toSubtitleMimeType(), sub.url.toSubtitleMimeType(),
langId, langId,
C.SELECTION_FLAG_DEFAULT C.SELECTION_FLAG_DEFAULT
)
) )
} )
activeSubtitles = subItemsId
mediaItemBuilder.setSubtitles(subItems)
} }
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 //might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps
val mediaItem = mediaItemBuilder.build() val mediaItem = mediaItemBuilder.build()
@ -1825,6 +1838,16 @@ class PlayerFragment : Fragment() {
}) })
} catch (e: java.lang.IllegalStateException) { } catch (e: java.lang.IllegalStateException) {
println("Warning: Illegal state exception in PlayerFragment") 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.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState 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.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.player.PlayerData import com.lagradost.cloudstream3.ui.player.PlayerData
import com.lagradost.cloudstream3.ui.player.PlayerFragment 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.*
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable 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.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
@ -239,7 +236,7 @@ class ResultFragment : Fragment() {
var startAction: Int? = null var startAction: Int? = null
private fun lateFixDownloadButton(show: Boolean) { private fun lateFixDownloadButton(show: Boolean) {
if(!show || currentType?.isMovieType() == false) { if (!show || currentType?.isMovieType() == false) {
result_movie_parent.visibility = GONE result_movie_parent.visibility = GONE
result_episodes_text.visibility = VISIBLE result_episodes_text.visibility = VISIBLE
result_episodes.visibility = VISIBLE result_episodes.visibility = VISIBLE
@ -381,7 +378,11 @@ class ResultFragment : Fragment() {
return false 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) val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setTitle(title) builder.setTitle(title)
@ -392,8 +393,8 @@ class ResultFragment : Fragment() {
builder.create().show() builder.create().show()
} }
fun aquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) { fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
aquireSingeExtractorLink(currentLinks ?: return, title, callback) acquireSingeExtractorLink(currentLinks ?: return, title, callback)
} }
fun startChromecast(startIndex: Int) { 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 isMovie = currentIsMovie ?: return
val titleName = sanitizeFilename(currentHeaderName ?: return) val titleName = sanitizeFilename(currentHeaderName ?: return)
@ -481,6 +482,36 @@ class ResultFragment : Fragment() {
meta, meta,
links 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() dialog.show()
} }
ACTION_COPY_LINK -> { ACTION_COPY_LINK -> {
aquireSingeExtractorLink("Copy Link") { link -> acquireSingeExtractorLink("Copy Link") { link ->
val serviceClipboard = val serviceClipboard =
(requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?) (requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?)
?: return@aquireSingeExtractorLink ?: return@acquireSingeExtractorLink
val clip = ClipData.newPlainText(link.name, link.url) val clip = ClipData.newPlainText(link.name, link.url)
serviceClipboard.setPrimaryClip(clip) serviceClipboard.setPrimaryClip(clip)
Toast.makeText(requireContext(), "Text Copied", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Text Copied", Toast.LENGTH_SHORT).show()
@ -558,7 +589,7 @@ class ResultFragment : Fragment() {
} }
ACTION_PLAY_EPISODE_IN_BROWSER -> { ACTION_PLAY_EPISODE_IN_BROWSER -> {
aquireSingeExtractorLink("Play in Browser") { link -> acquireSingeExtractorLink("Play in Browser") { link ->
val i = Intent(ACTION_VIEW) val i = Intent(ACTION_VIEW)
i.data = Uri.parse(link.url) i.data = Uri.parse(link.url)
startActivity(i) startActivity(i)
@ -566,7 +597,7 @@ class ResultFragment : Fragment() {
} }
ACTION_CHROME_CAST_MIRROR -> { ACTION_CHROME_CAST_MIRROR -> {
aquireSingeExtractorLink("Cast Mirror") { link -> acquireSingeExtractorLink("Cast Mirror") { link ->
val mirrorIndex = currentLinks?.indexOf(link) ?: -1 val mirrorIndex = currentLinks?.indexOf(link) ?: -1
startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex) startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex)
} }
@ -657,15 +688,15 @@ class ResultFragment : Fragment() {
} }
ACTION_DOWNLOAD_EPISODE -> { ACTION_DOWNLOAD_EPISODE -> {
startDownload(currentLinks ?: return@main) startDownload(currentLinks ?: return@main, currentSubs)
} }
ACTION_DOWNLOAD_MIRROR -> { ACTION_DOWNLOAD_MIRROR -> {
aquireSingeExtractorLink( acquireSingeExtractorLink(
(currentLinks ?: return@main).filter { !it.isM3u8 }, (currentLinks ?: return@main).filter { !it.isM3u8 },
"Download Mirror" "Download Mirror"
) { link -> ) { link ->
startDownload(listOf(link)) startDownload(listOf(link), currentSubs)
} }
} }
} }
@ -704,7 +735,7 @@ class ResultFragment : Fragment() {
} }
observe(viewModel.episodes) { episodeList -> 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) { when (startAction) {
START_ACTION_RESUME_LATEST -> { 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.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog 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.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.subtitle_settings.* import kotlinx.android.synthetic.main.subtitle_settings.*
const val SUBTITLE_KEY = "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( data class SaveCaptionStyle(
var foregroundColor: Int, var foregroundColor: Int,
@ -111,6 +116,14 @@ class SubtitlesFragment : Fragment() {
val metrics: DisplayMetrics = Resources.getSystem().displayMetrics val metrics: DisplayMetrics = Resources.getSystem().displayMetrics
return TypedValue.applyDimension(unit, size, metrics).toInt() 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>) { private fun onColorSelected(stuff: Pair<Int, Int>) {
@ -296,10 +309,57 @@ class SubtitlesFragment : Fragment() {
} }
} }
subs_font.setOnLongClickListener { subs_font.setOnLongClickListener { textView ->
state.typeface = null state.typeface = null
it.context.updateState() textView.context.updateState()
Toast.makeText(it.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show() 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 return@setOnLongClickListener true
} }

View file

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

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.util.forEach
import androidx.core.view.marginLeft import androidx.core.view.marginLeft
import androidx.core.view.marginRight import androidx.core.view.marginRight
import androidx.core.view.marginTop import androidx.core.view.marginTop
@ -15,20 +16,22 @@ object SingleSelectionHelper {
fun Context.showDialog( fun Context.showDialog(
dialog: Dialog, dialog: Dialog,
items: List<String>, items: List<String>,
selectedIndex: Int, selectedIndex: List<Int>,
name: String, name: String,
showApply: Boolean, showApply: Boolean,
callback: (Int) -> Unit, isMultiSelect: Boolean,
callback: (List<Int>) -> Unit,
dismissCallback: () -> Unit dismissCallback: () -> Unit
) { ) {
val realShowApply = showApply || isMultiSelect
val listView = dialog.findViewById<ListView>(R.id.listview1)!! val listView = dialog.findViewById<ListView>(R.id.listview1)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!! val textView = dialog.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!! val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!! val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!! val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
applyHolder.visibility = if (showApply) View.VISIBLE else View.GONE applyHolder.visibility = if (realShowApply) View.VISIBLE else View.GONE
if (!showApply) { if (!realShowApply) {
val params = listView.layoutParams as LinearLayout.LayoutParams val params = listView.layoutParams as LinearLayout.LayoutParams
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0) params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
listView.layoutParams = params listView.layoutParams = params
@ -40,29 +43,45 @@ object SingleSelectionHelper {
arrayAdapter.addAll(items) arrayAdapter.addAll(items)
listView.adapter = arrayAdapter listView.adapter = arrayAdapter
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE if (isMultiSelect) {
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
} else {
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
}
listView.setSelection(selectedIndex) for (select in selectedIndex) {
listView.setItemChecked(selectedIndex, true) listView.setItemChecked(select, true)
}
var currentIndex = selectedIndex selectedIndex.minOrNull()?.let {
listView.setSelection(it)
}
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
dialog.setOnDismissListener { dialog.setOnDismissListener {
dismissCallback.invoke() dismissCallback.invoke()
} }
listView.setOnItemClickListener { _, _, which, _ -> listView.setOnItemClickListener { _, _, which, _ ->
if (showApply) { // lastSelectedIndex = which
currentIndex = which if (realShowApply) {
listView.setItemChecked(which, true) if (!isMultiSelect) {
listView.setItemChecked(which, true)
}
} else { } else {
callback.invoke(which) callback.invoke(listOf(which))
dialog.dismiss() dialog.dismiss()
} }
} }
if (showApply) { if (realShowApply) {
applyButton.setOnClickListener { 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() dialog.dismiss()
} }
cancelButton.setOnClickListener { 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( fun Context.showDialog(
items: List<String>, items: List<String>,
selectedIndex: Int, selectedIndex: Int,
@ -84,7 +118,16 @@ object SingleSelectionHelper {
val dialog = builder.create() val dialog = builder.create()
dialog.show() 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( fun Context.showBottomDialog(
@ -100,6 +143,15 @@ object SingleSelectionHelper {
builder.setContentView(R.layout.bottom_selection_dialog) builder.setContentView(R.layout.bottom_selection_dialog)
builder.show() 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("Malayalam", "മലയാളം", "ml", "mal", "mal", "mal", ""),
Language639("Maltese", "Malti", "mt", "mlt", "mlt", "mlt", ""), Language639("Maltese", "Malti", "mt", "mlt", "mlt", "mlt", ""),
Language639("Māori", "te reo Māori", "mi", "mri", "", "mri", ""), 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("Marshallese", "Kajin M̧ajeļ", "mh", "mah", "mah", "mah", ""),
Language639("Mongolian", "Монгол хэл", "mn", "mon", "mon", "mon", ""), Language639("Mongolian", "Монгол хэл", "mn", "mon", "mon", "mon", ""),
Language639("Nauruan", "Dorerin Naoero", "na", "nau", "nau", "nau", ""), 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("Reunion Creole", "Kréol Rénioné", "rc", "rcf", "rcf", "rcf", ""),
Language639("Romanian", "limba română", "ro", "ron", "", "ron", ""), Language639("Romanian", "limba română", "ro", "ron", "", "ron", ""),
Language639("Russian", "Русский", "ru", "rus", "rus", "rus", ""), 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("Sardinian", "sardu", "sc", "srd", "srd", "srd", ""),
Language639("Sindhi", "सिन्धी, سنڌي، سندھی‎", "sd", "snd", "snd", "snd", ""), Language639("Sindhi", "सिन्धी, سنڌي، سندھی‎", "sd", "snd", "snd", "snd", ""),
Language639("Northern Sami", "Davvisámegiella", "se", "sme", "sme", "sme", ""), 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.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi 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.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getExistingDownloadUriOrNullQ
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -35,6 +37,7 @@ import java.lang.Thread.sleep
import java.net.URL import java.net.URL
import java.net.URLConnection import java.net.URLConnection
import java.util.* import java.util.*
import kotlin.collections.ArrayList
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
const val DOWNLOAD_CHANNEL_NAME = "Downloads" const val DOWNLOAD_CHANNEL_NAME = "Downloads"
@ -85,6 +88,15 @@ object VideoDownloadManager {
Stop, Stop,
} }
interface IDownloadableMinimum {
val url: String
val referer: String
}
fun VideoDownloadManager.IDownloadableMinimum.getId(): Int {
return url.hashCode()
}
data class DownloadEpisodeMetadata( data class DownloadEpisodeMetadata(
val id: Int, val id: Int,
val mainName: String, val mainName: String,
@ -126,7 +138,7 @@ object VideoDownloadManager {
private const val SUCCESS_DOWNLOAD_DONE = 1 private const val SUCCESS_DOWNLOAD_DONE = 1
private const val SUCCESS_STOPPED = 2 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_CREATE_FILE = -2
private const val ERROR_OPEN_FILE = -3 private const val ERROR_OPEN_FILE = -3
private const val ERROR_TOO_SMALL_CONNECTION = -4 private const val ERROR_TOO_SMALL_CONNECTION = -4
@ -191,7 +203,7 @@ object VideoDownloadManager {
cachedBitmaps[url] = bitmap cachedBitmaps[url] = bitmap
} }
return null return null
} catch (e : Exception) { } catch (e: Exception) {
return null return null
} }
} }
@ -361,6 +373,69 @@ object VideoDownloadManager {
return tempName.replace(" ", " ").trim(' ') 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) @RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? { private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
try { try {
@ -412,18 +487,24 @@ object VideoDownloadManager {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
} }
private fun downloadSingleEpisode( data class CreateNotificationMetadata(
context: Context, val type: DownloadType,
source: String?, val bytesDownloaded: Long,
folder: String?, val bytesTotal: Long,
ep: DownloadEpisodeMetadata, )
link: ExtractorLink,
tryResume: Boolean = false,
): Int {
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
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 relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
val displayName = "$name.mp4" val displayName = "$name.$extension"
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName" val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
var resume = tryResume var resume = tryResume
@ -440,7 +521,9 @@ object VideoDownloadManager {
} else { } else {
if (!File(normalPath).delete()) return ERROR_DELETING_FILE if (!File(normalPath).delete()) return ERROR_DELETING_FILE
} }
downloadDeleteEvent.invoke(ep.id) parentId?.let {
downloadDeleteEvent.invoke(parentId)
}
return SUCCESS_STOPPED return SUCCESS_STOPPED
} }
@ -468,11 +551,18 @@ object VideoDownloadManager {
} else { } else {
val contentUri = val contentUri =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI 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 { val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, name) 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) put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
} }
@ -539,9 +629,11 @@ object VideoDownloadManager {
} }
val bytesTotal = contentLength + resumeLength 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, // Could use connection.contentType for mime types when creating the file,
// however file is already created and players don't go of file type // however file is already created and players don't go of file type
@ -573,15 +665,18 @@ object VideoDownloadManager {
else -> DownloadType.IsDownloading else -> DownloadType.IsDownloading
} }
try { parentId?.let { id ->
downloadStatus[ep.id] = type try {
downloadStatusEvent.invoke(Pair(ep.id, type)) downloadStatus[id] = type
downloadProgressEvent.invoke(Triple(ep.id, bytesDownloaded, bytesTotal)) downloadStatusEvent.invoke(Pair(id, type))
} catch (e: Exception) { downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
// IDK MIGHT ERROR } catch (e: Exception) {
// IDK MIGHT ERROR
}
} }
createNotification( createNotificationCallback.invoke(CreateNotificationMetadata(type, bytesDownloaded, bytesTotal))
/*createNotification(
context, context,
source, source,
link.name, link.name,
@ -589,12 +684,11 @@ object VideoDownloadManager {
type, type,
bytesDownloaded, bytesDownloaded,
bytesTotal bytesTotal
) )*/
} }
val downloadEventListener = { event: Pair<Int, DownloadActionType> -> val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
if (event.first == ep.id) { if (event.first == parentId) {
when (event.second) { when (event.second) {
DownloadActionType.Pause -> { DownloadActionType.Pause -> {
isPaused = true; updateNotification() isPaused = true; updateNotification()
@ -611,7 +705,8 @@ object VideoDownloadManager {
} }
} }
downloadEvent += downloadEventListener if (parentId != null)
downloadEvent += downloadEventListener
// UPDATE DOWNLOAD NOTIFICATION // UPDATE DOWNLOAD NOTIFICATION
val notificationCoroutine = main { val notificationCoroutine = main {
@ -625,7 +720,6 @@ object VideoDownloadManager {
} }
} }
val id = ep.id
// THE REAL READ // THE REAL READ
try { try {
while (true) { while (true) {
@ -655,13 +749,16 @@ object VideoDownloadManager {
notificationCoroutine.cancel() notificationCoroutine.cancel()
try { try {
downloadEvent -= downloadEventListener if (parentId != null)
downloadEvent -= downloadEventListener
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
try { try {
downloadStatus.remove(ep.id) parentId?.let {
downloadStatus.remove(it)
}
} catch (e: Exception) { } catch (e: Exception) {
// IDK MIGHT ERROR // IDK MIGHT ERROR
} }
@ -669,15 +766,15 @@ object VideoDownloadManager {
// RETURN MESSAGE // RETURN MESSAGE
return when { return when {
isFailed -> { isFailed -> {
downloadProgressEvent.invoke(Triple(id, 0, 0)) parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
ERROR_CONNECTION_ERROR ERROR_CONNECTION_ERROR
} }
isStopped -> { isStopped -> {
downloadProgressEvent.invoke(Triple(id, 0, 0)) parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
deleteFile() deleteFile()
} }
else -> { else -> {
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) }
isDone = true isDone = true
updateNotification() updateNotification()
SUCCESS_DOWNLOAD_DONE 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) { private fun downloadCheck(context: Context) {
if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
val pkg = downloadQueue.removeFirst() val pkg = downloadQueue.removeFirst()

View file

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

View file

@ -12,8 +12,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingStart="20dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:paddingEnd="20dp"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:textStyle="bold" android:textStyle="bold"
@ -78,6 +78,26 @@
android:text="@string/subs_subtitle_elevation" android:text="@string/subs_subtitle_elevation"
style="@style/SettingsItem" 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 <LinearLayout
android:orientation="horizontal" android:orientation="horizontal"

View file

@ -88,4 +88,7 @@
<string name="manual_check_update_key">manual_check_update</string> <string name="manual_check_update_key">manual_check_update</string>
<string name="prerelease_commit_hash">unknown_prerelease</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> </resources>