From 10f25eaefe3e2934a12a18b96451198ff4403fcc Mon Sep 17 00:00:00 2001 From: LagradOst Date: Sun, 22 Aug 2021 19:14:48 +0200 Subject: [PATCH] subtitle download and other sub settings --- .../ui/download/DownloadButtonSetup.kt | 8 +- .../cloudstream3/ui/player/PlayerFragment.kt | 317 ++++++++++-------- .../cloudstream3/ui/result/ResultFragment.kt | 67 +++- .../ui/subtitles/SubtitlesFragment.kt | 66 +++- .../cloudstream3/utils/ExtractorApi.kt | 14 +- .../utils/SingleSelectionHelper.kt | 84 ++++- .../cloudstream3/utils/SubtitleHelper.kt | 4 +- .../utils/VideoDownloadManager.kt | 188 +++++++++-- .../layout/player_select_source_and_subs.xml | 1 + app/src/main/res/layout/subtitle_settings.xml | 24 +- app/src/main/res/values/strings.xml | 3 + 11 files changed, 547 insertions(+), 229 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 688fa287..4041a36f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -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.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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt index cf80cfe6..a37660cf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt @@ -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,136 +1192,97 @@ 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 + val isPlaying = exoPlayer.isPlaying + exoPlayer.pause() + val currentSubtitles = activeSubtitles - val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) - .setView(R.layout.player_select_source_and_subs) + val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) + .setView(R.layout.player_select_source_and_subs) - val sourceDialog = sourceBuilder.create() - sourceDialog.show() - // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) - val providerList = sourceDialog.findViewById(R.id.sort_providers)!! - val subtitleList = sourceDialog.findViewById(R.id.sort_subtitles)!! - val applyButton = sourceDialog.findViewById(R.id.apply_btt)!! - val cancelButton = sourceDialog.findViewById(R.id.cancel_btt)!! - val subsSettings = sourceDialog.findViewById(R.id.subs_settings)!! + val sourceDialog = sourceBuilder.create() + sourceDialog.show() + // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) + val providerList = sourceDialog.findViewById(R.id.sort_providers)!! + val subtitleList = sourceDialog.findViewById(R.id.sort_subtitles)!! + val applyButton = sourceDialog.findViewById(R.id.apply_btt)!! + val cancelButton = sourceDialog.findViewById(R.id.cancel_btt)!! + val subsSettings = sourceDialog.findViewById(R.id.subs_settings)!! - subsSettings.setOnClickListener { - SubtitlesFragment.push(activity) - sourceDialog.dismiss() - } + subsSettings.setOnClickListener { + SubtitlesFragment.push(activity) + sourceDialog.dismiss() + } + var sourceIndex = 0 + var startSource = 0 + var sources: List = 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(R.id.sort_sources_holder)?.visibility = GONE + } else { + sources = sortUrls(nonSortedUrls) + startSource = sources.indexOf(getCurrentUrl()) + sourceIndex = startSource - if (currentSubtitles.isEmpty()) { - sourceDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = GONE - } else { - val subsArrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) - subsArrayAdapter.add("No Subtitles") - subsArrayAdapter.addAll(currentSubtitles) + val sourcesArrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + sourcesArrayAdapter.addAll(sources.map { it.name }) - subtitleList.adapter = subsArrayAdapter - subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE + providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE + providerList.adapter = sourcesArrayAdapter + providerList.setSelection(sourceIndex) + providerList.setItemChecked(sourceIndex, true) - subtitleList.setSelection(subtitleIndex) - subtitleList.setItemChecked(subtitleIndex, true) - - subtitleList.setOnItemClickListener { _, _, which, _ -> - subtitleIndex = which - subtitleList.setItemChecked(which, true) - } - } - - val sourcesArrayAdapter = ArrayAdapter(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()*/ + 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(R.id.sort_subtitles_holder)?.visibility = GONE + } else { + val subsArrayAdapter = ArrayAdapter(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? { + private fun Context.getSubs(supportsDownloadedFiles: Boolean = true): List? { return try { - allEpisodesSubs[getEpisode()?.id] + if (isDownloadedFile) { + if (!supportsDownloadedFiles) return null + val list = ArrayList() + 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,28 +1614,26 @@ class PlayerFragment : Fragment() { } } - val subs = getSubs() - if (subs != null) { - val subItems = ArrayList() - val subItemsId = ArrayList() + val subs = context?.getSubs() ?: emptyList() + val subItems = ArrayList() + val subItemsId = ArrayList() - for (sub in sortSubs(subs)) { - val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang - subItemsId.add(langId) - subItems.add( - MediaItem.Subtitle( - Uri.parse(sub.url), - sub.url.toSubtitleMimeType(), - langId, - C.SELECTION_FLAG_DEFAULT - ) + for (sub in sortSubs(subs)) { + val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang + subItemsId.add(langId) + subItems.add( + MediaItem.Subtitle( + Uri.parse(sub.url), + sub.url.toSubtitleMimeType(), + langId, + 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 val mediaItem = mediaItemBuilder.build() @@ -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 + } + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 2dea1a1d..b20933e9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -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, title: String, callback: (ExtractorLink) -> Unit) { + fun acquireSingeExtractorLink( + links: List, + 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) { + fun startDownload(links: List, subs: List?) { 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 -> { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 6bf2a60c..bca44297 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -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 { + return getKey(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en") + } + + fun Context.getAutoSelectLanguageISO639_1(): String { + return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" + } } private fun onColorSelected(stuff: Pair) { @@ -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 } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index e42fe747..bfdaa9d9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -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), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 2128ab4c..09d3f3b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -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, - selectedIndex: Int, + selectedIndex: List, name: String, showApply: Boolean, - callback: (Int) -> Unit, + isMultiSelect: Boolean, + callback: (List) -> Unit, dismissCallback: () -> Unit ) { + val realShowApply = showApply || isMultiSelect val listView = dialog.findViewById(R.id.listview1)!! val textView = dialog.findViewById(R.id.text1)!! val applyButton = dialog.findViewById(R.id.apply_btt)!! val cancelButton = dialog.findViewById(R.id.cancel_btt)!! val applyHolder = dialog.findViewById(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 - listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE + 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 - listView.setItemChecked(which, true) + // 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() + 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, + selectedIndex: List, + name: String, + dismissCallback: () -> Unit, + callback: (List) -> 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, 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 + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt index b2b18b71..ad462b8b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt @@ -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", ""), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 13b212f2..3771e38f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -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>? { + 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>() + + 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>? { + 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 } - try { - downloadStatus[ep.id] = type - downloadStatusEvent.invoke(Pair(ep.id, type)) - downloadProgressEvent.invoke(Triple(ep.id, bytesDownloaded, bytesTotal)) - } catch (e: Exception) { - // IDK MIGHT ERROR + parentId?.let { id -> + try { + 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 -> - if (event.first == ep.id) { + if (event.first == parentId) { when (event.second) { DownloadActionType.Pause -> { isPaused = true; updateNotification() @@ -611,7 +705,8 @@ object VideoDownloadManager { } } - downloadEvent += downloadEventListener + if (parentId != null) + downloadEvent += downloadEventListener // UPDATE DOWNLOAD NOTIFICATION val notificationCoroutine = main { @@ -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 { - downloadEvent -= downloadEventListener + 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() diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index d86609cb..3e96c0a7 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -14,6 +14,7 @@ android:baselineAligned="false"> + + + + manual_check_update unknown_prerelease + Auto Select Language + Download Languages + Hold to reset to default \ No newline at end of file