mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Added custom download directory (#219)
Co-authored-by: Osten <balt.758@gmail.com>
This commit is contained in:
parent
13ba73bbd4
commit
e5f3d4b20b
10 changed files with 390 additions and 116 deletions
|
@ -86,14 +86,14 @@ dependencies {
|
||||||
testImplementation 'org.json:json:20180813'
|
testImplementation 'org.json:json:20180813'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.6.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
@ -148,4 +148,7 @@ dependencies {
|
||||||
// Networking
|
// Networking
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.9.1"
|
implementation "com.squareup.okhttp3:okhttp:4.9.1"
|
||||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
|
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
|
||||||
|
|
||||||
|
// Util to skip the URI file fuckery 🙏
|
||||||
|
implementation "com.github.tachiyomiorg:unifile:17bec43"
|
||||||
}
|
}
|
|
@ -98,6 +98,7 @@ object DownloadButtonSetup {
|
||||||
R.id.global_to_navigation_player, PlayerFragment.newInstance(
|
R.id.global_to_navigation_player, PlayerFragment.newInstance(
|
||||||
UriData(
|
UriData(
|
||||||
info.path.toString(),
|
info.path.toString(),
|
||||||
|
keyInfo.basePath,
|
||||||
keyInfo.relativePath,
|
keyInfo.relativePath,
|
||||||
keyInfo.displayName,
|
keyInfo.displayName,
|
||||||
click.data.parentId,
|
click.data.parentId,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.isMovieType
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DataStore
|
import com.lagradost.cloudstream3.utils.DataStore
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
|
@ -44,9 +45,11 @@ class DownloadFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setList(list: List<VisualDownloadHeaderCached>) {
|
private fun setList(list: List<VisualDownloadHeaderCached>) {
|
||||||
|
main {
|
||||||
(download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list
|
(download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list
|
||||||
download_list?.adapter?.notifyDataSetChanged()
|
download_list?.adapter?.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
(download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter()
|
(download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter()
|
||||||
|
|
|
@ -78,7 +78,6 @@ import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAu
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle
|
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
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.isCastApiAvailable
|
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.onAudioFocusEvent
|
import com.lagradost.cloudstream3.utils.AppUtils.onAudioFocusEvent
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
|
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
|
||||||
|
@ -108,6 +107,7 @@ import kotlin.math.abs
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
|
||||||
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
|
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
|
||||||
const val STATE_RESUME_WINDOW = "resumeWindow"
|
const val STATE_RESUME_WINDOW = "resumeWindow"
|
||||||
const val STATE_RESUME_POSITION = "resumePosition"
|
const val STATE_RESUME_POSITION = "resumePosition"
|
||||||
|
@ -158,6 +158,7 @@ data class PlayerData(
|
||||||
|
|
||||||
data class UriData(
|
data class UriData(
|
||||||
val uri: String,
|
val uri: String,
|
||||||
|
val basePath: String?,
|
||||||
val relativePath: String,
|
val relativePath: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val parentId: Int?,
|
val parentId: Int?,
|
||||||
|
@ -1608,7 +1609,7 @@ class PlayerFragment : Fragment() {
|
||||||
if (isDownloadedFile) {
|
if (isDownloadedFile) {
|
||||||
if (!supportsDownloadedFiles) return null
|
if (!supportsDownloadedFiles) return null
|
||||||
val list = ArrayList<SubtitleFile>()
|
val list = ArrayList<SubtitleFile>()
|
||||||
VideoDownloadManager.getFolder(this, uriData.relativePath)?.forEach { file ->
|
VideoDownloadManager.getFolder(this, uriData.relativePath, uriData.basePath)?.forEach { file ->
|
||||||
val name = uriData.displayName.removeSuffix(".mp4")
|
val name = uriData.displayName.removeSuffix(".mp4")
|
||||||
if (file.first != uriData.displayName && file.first.startsWith(name)) {
|
if (file.first != uriData.displayName && file.first.startsWith(name)) {
|
||||||
val realName = file.first.removePrefix(name)
|
val realName = file.first.removePrefix(name)
|
||||||
|
@ -1882,15 +1883,7 @@ class PlayerFragment : Fragment() {
|
||||||
mediaItemBuilder.setUri(currentUrl.url)
|
mediaItemBuilder.setUri(currentUrl.url)
|
||||||
} else if (trueUri != null || uri != null) {
|
} else if (trueUri != null || uri != null) {
|
||||||
val uriPrimary = trueUri ?: Uri.parse(uri)
|
val uriPrimary = trueUri ?: Uri.parse(uri)
|
||||||
if (uriPrimary.scheme == "content") {
|
|
||||||
mediaItemBuilder.setUri(uriPrimary)
|
mediaItemBuilder.setUri(uriPrimary)
|
||||||
// video_title?.text = uriPrimary.toString()
|
|
||||||
} else {
|
|
||||||
//mediaItemBuilder.setUri(Uri.parse(currentUrl.url))
|
|
||||||
val realUri = trueUri ?: getVideoContentUri(requireContext(), uri ?: uriPrimary.path ?: "")
|
|
||||||
// video_title?.text = uri.toString()
|
|
||||||
mediaItemBuilder.setUri(realUri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val subs = context?.getSubs() ?: emptyList()
|
val subs = context?.getSubs() ?: emptyList()
|
||||||
|
@ -2214,7 +2207,8 @@ class PlayerFragment : Fragment() {
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun initPlayer() {
|
private fun initPlayer() {
|
||||||
if (isDownloadedFile) {
|
if (isDownloadedFile) {
|
||||||
initPlayer(null, uriData.uri.removePrefix("file://").replace("%20", " ")) // FIX FILE PERMISSION
|
//.removePrefix("file://").replace("%20", " ") // FIX FILE PERMISSION
|
||||||
|
initPlayer(null, uriData.uri)
|
||||||
}
|
}
|
||||||
println("INIT PLAYER")
|
println("INIT PLAYER")
|
||||||
view?.setOnTouchListener { _, _ -> return@setOnTouchListener true } // VERY IMPORTANT https://stackoverflow.com/questions/28818926/prevent-clicking-on-a-button-in-an-activity-while-showing-a-fragment
|
view?.setOnTouchListener { _, _ -> return@setOnTouchListener true } // VERY IMPORTANT https://stackoverflow.com/questions/28818926/prevent-clicking-on-a-button-in-an-activity-while-showing-a-fragment
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings
|
package com.lagradost.cloudstream3.ui.settings
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.app.UiModeManager
|
import android.app.UiModeManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.restrictedApis
|
import com.lagradost.cloudstream3.APIHolder.restrictedApis
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication
|
||||||
import com.lagradost.cloudstream3.DubStatus
|
import com.lagradost.cloudstream3.DubStatus
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.setLocale
|
import com.lagradost.cloudstream3.MainActivity.Companion.setLocale
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.showToast
|
import com.lagradost.cloudstream3.MainActivity.Companion.showToast
|
||||||
|
@ -32,6 +39,10 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadDir
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.isScopedStorage
|
||||||
|
import java.io.File
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,6 +65,33 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private var beneneCount = 0
|
private var beneneCount = 0
|
||||||
|
|
||||||
|
// Open file picker
|
||||||
|
private val pathPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||||
|
// It lies, it can be null if file manager quits.
|
||||||
|
if (uri == null) return@registerForActivityResult
|
||||||
|
val context = context ?: AcraApplication.context ?: return@registerForActivityResult
|
||||||
|
// RW perms for the path
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
println("Selected URI path: $uri - Full path: ${file.filePath}")
|
||||||
|
|
||||||
|
// Stores the real URI using download_path_key
|
||||||
|
// Important that the URI is stored instead of filepath due to permissions.
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit().putString(getString(R.string.download_path_key), uri.toString()).apply()
|
||||||
|
|
||||||
|
// From URI -> File path
|
||||||
|
// File path here is purely for cosmetic purposes in settings
|
||||||
|
(file.filePath ?: uri.toString()).let {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit().putString(getString(R.string.download_path_pref), it).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// idk, if you find a way of automating this it would be great
|
// idk, if you find a way of automating this it would be great
|
||||||
private val languages = arrayListOf(
|
private val languages = arrayListOf(
|
||||||
Triple("\uD83C\uDDEA\uD83C\uDDF8", "Spanish", "es"),
|
Triple("\uD83C\uDDEA\uD83C\uDDF8", "Spanish", "es"),
|
||||||
|
@ -84,6 +122,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!!
|
val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!!
|
||||||
val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!!
|
val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!!
|
||||||
val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!!
|
val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!!
|
||||||
|
val downloadPathPreference = findPreference<Preference>(getString(R.string.download_path_key))!!
|
||||||
val allLayoutPreference = findPreference<Preference>(getString(R.string.app_layout_key))!!
|
val allLayoutPreference = findPreference<Preference>(getString(R.string.app_layout_key))!!
|
||||||
val colorPrimaryPreference = findPreference<Preference>(getString(R.string.primary_color_key))!!
|
val colorPrimaryPreference = findPreference<Preference>(getString(R.string.primary_color_key))!!
|
||||||
val preferedMediaTypePreference = findPreference<Preference>(getString(R.string.prefer_media_type_key))!!
|
val preferedMediaTypePreference = findPreference<Preference>(getString(R.string.prefer_media_type_key))!!
|
||||||
|
@ -168,6 +207,48 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
return@setOnPreferenceClickListener true
|
return@setOnPreferenceClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDownloadDirs(): List<String> {
|
||||||
|
val defaultDir = getDownloadDir()?.filePath
|
||||||
|
|
||||||
|
// app_name_download_path = Cloudstream and does not change depending on release.
|
||||||
|
// DOES NOT WORK ON SCOPED STORAGE.
|
||||||
|
val secondaryDir = if (isScopedStorage) null else Environment.getExternalStorageDirectory().absolutePath +
|
||||||
|
File.separator + resources.getString(R.string.app_name_download_path)
|
||||||
|
|
||||||
|
val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second }
|
||||||
|
|
||||||
|
return (listOf(defaultDir, secondaryDir) +
|
||||||
|
requireContext().getExternalFilesDirs("").mapNotNull { it.path } +
|
||||||
|
currentDir).filterNotNull().distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadPathPreference.setOnPreferenceClickListener {
|
||||||
|
val dirs = getDownloadDirs()
|
||||||
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val currentDir =
|
||||||
|
settingsManager.getString(getString(R.string.download_path_pref), null) ?: getDownloadDir().toString()
|
||||||
|
|
||||||
|
context?.showBottomDialog(
|
||||||
|
dirs + listOf("Custom"),
|
||||||
|
dirs.indexOf(currentDir),
|
||||||
|
getString(R.string.download_path_pref),
|
||||||
|
true,
|
||||||
|
{}) {
|
||||||
|
// Last = custom
|
||||||
|
if (it == dirs.size) {
|
||||||
|
pathPicker.launch(Uri.EMPTY)
|
||||||
|
} else {
|
||||||
|
// Sets both visual and actual paths.
|
||||||
|
// key = used path
|
||||||
|
// pref = visual path
|
||||||
|
settingsManager.edit().putString(getString(R.string.download_path_key), dirs[it]).apply()
|
||||||
|
settingsManager.edit().putString(getString(R.string.download_path_pref), dirs[it]).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@setOnPreferenceClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
preferedMediaTypePreference.setOnPreferenceClickListener {
|
preferedMediaTypePreference.setOnPreferenceClickListener {
|
||||||
val prefNames = resources.getStringArray(R.array.media_type_pref)
|
val prefNames = resources.getStringArray(R.array.media_type_pref)
|
||||||
val prefValues = resources.getIntArray(R.array.media_type_pref_values)
|
val prefValues = resources.getIntArray(R.array.media_type_pref_values)
|
||||||
|
@ -206,7 +287,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
true,
|
true,
|
||||||
{}) {
|
{}) {
|
||||||
try {
|
try {
|
||||||
settingsManager.edit().putInt(getString(R.string.app_layout_key), prefValues[it]).apply()
|
settingsManager.edit().putInt(getString(R.string.app_layout_key), prefValues[it])
|
||||||
|
.apply()
|
||||||
activity?.recreate()
|
activity?.recreate()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -220,7 +302,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val prefValues = resources.getStringArray(R.array.themes_overlay_names_values)
|
val prefValues = resources.getStringArray(R.array.themes_overlay_names_values)
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
val currentLayout = settingsManager.getString( getString(R.string.primary_color_key),prefValues.first())
|
val currentLayout =
|
||||||
|
settingsManager.getString(getString(R.string.primary_color_key), prefValues.first())
|
||||||
context?.showBottomDialog(
|
context?.showBottomDialog(
|
||||||
prefNames.toList(),
|
prefNames.toList(),
|
||||||
prefValues.indexOf(currentLayout),
|
prefValues.indexOf(currentLayout),
|
||||||
|
@ -228,7 +311,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
true,
|
true,
|
||||||
{}) {
|
{}) {
|
||||||
try {
|
try {
|
||||||
settingsManager.edit().putString(getString(R.string.primary_color_key), prefValues[it]).apply()
|
settingsManager.edit().putString(getString(R.string.primary_color_key), prefValues[it])
|
||||||
|
.apply()
|
||||||
activity?.recreate()
|
activity?.recreate()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -242,7 +326,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val prefValues = resources.getStringArray(R.array.themes_names_values)
|
val prefValues = resources.getStringArray(R.array.themes_names_values)
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
val currentLayout = settingsManager.getString( getString(R.string.app_theme_key),prefValues.first())
|
val currentLayout =
|
||||||
|
settingsManager.getString(getString(R.string.app_theme_key), prefValues.first())
|
||||||
context?.showBottomDialog(
|
context?.showBottomDialog(
|
||||||
prefNames.toList(),
|
prefNames.toList(),
|
||||||
prefValues.indexOf(currentLayout),
|
prefValues.indexOf(currentLayout),
|
||||||
|
@ -250,7 +335,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
true,
|
true,
|
||||||
{}) {
|
{}) {
|
||||||
try {
|
try {
|
||||||
settingsManager.edit().putString(getString(R.string.app_theme_key), prefValues[it]).apply()
|
settingsManager.edit().putString(getString(R.string.app_theme_key), prefValues[it])
|
||||||
|
.apply()
|
||||||
activity?.recreate()
|
activity?.recreate()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -265,14 +351,18 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
val currentQuality =
|
val currentQuality =
|
||||||
settingsManager.getInt(getString(R.string.watch_quality_pref), Qualities.values().last().value)
|
settingsManager.getInt(
|
||||||
|
getString(R.string.watch_quality_pref),
|
||||||
|
Qualities.values().last().value
|
||||||
|
)
|
||||||
context?.showBottomDialog(
|
context?.showBottomDialog(
|
||||||
prefNames.toList(),
|
prefNames.toList(),
|
||||||
prefValues.indexOf(currentQuality),
|
prefValues.indexOf(currentQuality),
|
||||||
getString(R.string.watch_quality_pref),
|
getString(R.string.watch_quality_pref),
|
||||||
true,
|
true,
|
||||||
{}) {
|
{}) {
|
||||||
settingsManager.edit().putInt(getString(R.string.watch_quality_pref), prefValues[it]).apply()
|
settingsManager.edit().putInt(getString(R.string.watch_quality_pref), prefValues[it])
|
||||||
|
.apply()
|
||||||
}
|
}
|
||||||
return@setOnPreferenceClickListener true
|
return@setOnPreferenceClickListener true
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||||
|
@ -33,7 +34,6 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo
|
||||||
val info = applicationContext.getKey<VideoDownloadManager.DownloadInfo>(WORK_KEY_INFO, key)
|
val info = applicationContext.getKey<VideoDownloadManager.DownloadInfo>(WORK_KEY_INFO, key)
|
||||||
val pkg =
|
val pkg =
|
||||||
applicationContext.getKey<VideoDownloadManager.DownloadResumePackage>(WORK_KEY_PACKAGE, key)
|
applicationContext.getKey<VideoDownloadManager.DownloadResumePackage>(WORK_KEY_PACKAGE, key)
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
downloadEpisode(
|
downloadEpisode(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
|
@ -44,6 +44,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo
|
||||||
::handleNotification
|
::handleNotification
|
||||||
)
|
)
|
||||||
awaitDownload(info.ep.id)
|
awaitDownload(info.ep.id)
|
||||||
|
|
||||||
} else if (pkg != null) {
|
} else if (pkg != null) {
|
||||||
downloadFromResume(applicationContext, pkg, ::handleNotification)
|
downloadFromResume(applicationContext, pkg, ::handleNotification)
|
||||||
awaitDownload(pkg.item.ep.id)
|
awaitDownload(pkg.item.ep.id)
|
||||||
|
@ -52,6 +53,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo
|
||||||
}
|
}
|
||||||
return Result.success()
|
return Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
removeKeys(key)
|
removeKeys(key)
|
||||||
}
|
}
|
||||||
|
@ -79,6 +81,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo
|
||||||
}
|
}
|
||||||
downloadStatusEvent += listener
|
downloadStatusEvent += listener
|
||||||
while (!isDone) {
|
while (!isDone) {
|
||||||
|
println("AWAITING $id")
|
||||||
delay(1000)
|
delay(1000)
|
||||||
}
|
}
|
||||||
downloadStatusEvent -= listener
|
downloadStatusEvent -= listener
|
||||||
|
|
|
@ -15,11 +15,13 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import com.lagradost.cloudstream3.MainActivity
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
@ -33,7 +35,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.*
|
import okhttp3.internal.closeQuietly
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
import java.lang.Thread.sleep
|
import java.lang.Thread.sleep
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
@ -126,7 +132,8 @@ object VideoDownloadManager {
|
||||||
val totalBytes: Long,
|
val totalBytes: Long,
|
||||||
val relativePath: String,
|
val relativePath: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val extraInfo: String? = null
|
val extraInfo: String? = null,
|
||||||
|
val basePath: String? = null // null is for legacy downloads. See getDefaultPath()
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DownloadedFileInfoResult(
|
data class DownloadedFileInfoResult(
|
||||||
|
@ -210,6 +217,7 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -433,25 +441,34 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFolder(context: Context, relativePath: String): List<Pair<String, Uri>>? {
|
/**
|
||||||
if (isScopedStorage()) {
|
* Used for getting video player subs.
|
||||||
|
* @return List of pairs for the files in this format: <Name, Uri>
|
||||||
|
* */
|
||||||
|
fun getFolder(context: Context, relativePath: String, basePath: String?): List<Pair<String, Uri>>? {
|
||||||
|
val base = basePathToFile(context, basePath)
|
||||||
|
val folder = base?.gotoDir(relativePath, false)
|
||||||
|
|
||||||
|
if (isScopedStorage && base.isDownloadDir()) {
|
||||||
return context.contentResolver?.getExistingFolderStartName(relativePath)
|
return context.contentResolver?.getExistingFolderStartName(relativePath)
|
||||||
} else {
|
} else {
|
||||||
val normalPath =
|
// val normalPath =
|
||||||
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace(
|
// "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace(
|
||||||
'/',
|
// '/',
|
||||||
File.separatorChar
|
// File.separatorChar
|
||||||
)
|
// )
|
||||||
val folder = File(normalPath)
|
// val folder = File(normalPath)
|
||||||
if (folder.isDirectory) {
|
if (folder?.isDirectory == true) {
|
||||||
return folder.listFiles()?.map { Pair(it.name, it.toUri()) }
|
return folder.listFiles()?.map { Pair(it.name ?: "", it.uri) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
@ -487,6 +504,7 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -497,13 +515,14 @@ object VideoDownloadManager {
|
||||||
this.openFileDescriptor(fileUri, "r")
|
this.openFileDescriptor(fileUri, "r")
|
||||||
.use { it?.statSize ?: 0 }
|
.use { it?.statSize ?: 0 }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isScopedStorage(): Boolean {
|
val isScopedStorage: Boolean
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||||
}
|
|
||||||
|
|
||||||
data class CreateNotificationMetadata(
|
data class CreateNotificationMetadata(
|
||||||
val type: DownloadType,
|
val type: DownloadType,
|
||||||
|
@ -518,6 +537,10 @@ object VideoDownloadManager {
|
||||||
val fileStream: OutputStream? = null,
|
val fileStream: OutputStream? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the appropriate file and creates a data stream from the file.
|
||||||
|
* Used for initializing downloads.
|
||||||
|
* */
|
||||||
private fun setupStream(
|
private fun setupStream(
|
||||||
context: Context,
|
context: Context,
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -525,16 +548,17 @@ object VideoDownloadManager {
|
||||||
extension: String,
|
extension: String,
|
||||||
tryResume: Boolean,
|
tryResume: Boolean,
|
||||||
): StreamData {
|
): StreamData {
|
||||||
val relativePath = getRelativePath(folder)
|
|
||||||
val displayName = getDisplayName(name, extension)
|
val displayName = getDisplayName(name, extension)
|
||||||
val fileStream: OutputStream
|
val fileStream: OutputStream
|
||||||
val fileLength: Long
|
val fileLength: Long
|
||||||
var resume = tryResume
|
var resume = tryResume
|
||||||
if (isScopedStorage()) {
|
val baseFile = context.getBasePath()
|
||||||
|
|
||||||
|
if (isScopedStorage && baseFile.first?.isDownloadDir() == true) {
|
||||||
val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||||
|
|
||||||
val currentExistingFile =
|
val currentExistingFile =
|
||||||
cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
|
cr.getExistingDownloadUriOrNullQ(folder ?: "", displayName) // CURRENT FILE WITH THE SAME PATH
|
||||||
|
|
||||||
fileLength =
|
fileLength =
|
||||||
if (currentExistingFile == null || !resume) 0 else (cr.getFileLength(currentExistingFile)
|
if (currentExistingFile == null || !resume) 0 else (cr.getFileLength(currentExistingFile)
|
||||||
|
@ -566,7 +590,7 @@ object VideoDownloadManager {
|
||||||
put(MediaStore.MediaColumns.TITLE, name)
|
put(MediaStore.MediaColumns.TITLE, name)
|
||||||
if (currentMimeType != null)
|
if (currentMimeType != null)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType)
|
put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType)
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
cr.insert(
|
cr.insert(
|
||||||
|
@ -578,26 +602,24 @@ object VideoDownloadManager {
|
||||||
fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else ""))
|
fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else ""))
|
||||||
?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||||
} else {
|
} else {
|
||||||
val normalPath = getNormalPath(relativePath, displayName)
|
val subDir = baseFile.first?.gotoDir(folder)
|
||||||
// NORMAL NON SCOPED STORAGE FILE CREATION
|
val rFile = subDir?.findFile(displayName)
|
||||||
val rFile = File(normalPath)
|
if (rFile?.exists() != true) {
|
||||||
if (!rFile.exists()) {
|
|
||||||
fileLength = 0
|
fileLength = 0
|
||||||
rFile.parentFile?.mkdirs()
|
if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE)
|
||||||
if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
|
||||||
} else {
|
} else {
|
||||||
if (resume) {
|
if (resume) {
|
||||||
fileLength = rFile.length()
|
fileLength = rFile.size()
|
||||||
} else {
|
} else {
|
||||||
fileLength = 0
|
fileLength = 0
|
||||||
rFile.parentFile?.mkdirs()
|
if (!rFile.delete()) return StreamData(ERROR_DELETING_FILE)
|
||||||
if (!rFile.delete()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
if (subDir.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE)
|
||||||
if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileStream = FileOutputStream(rFile, false)
|
fileStream = (subDir.findFile(displayName) ?: subDir.createFile(displayName))!!.openOutputStream()
|
||||||
}
|
// fileStream = FileOutputStream(rFile, false)
|
||||||
if (fileLength == 0L) resume = false
|
if (fileLength == 0L) resume = false
|
||||||
|
}
|
||||||
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
|
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -615,15 +637,16 @@ object VideoDownloadManager {
|
||||||
return ERROR_UNKNOWN
|
return ERROR_UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
val relativePath = getRelativePath(folder)
|
val basePath = context.getBasePath()
|
||||||
val displayName = getDisplayName(name, extension)
|
|
||||||
|
|
||||||
|
val displayName = getDisplayName(name, extension)
|
||||||
|
val relativePath = if (isScopedStorage && basePath.first.isDownloadDir()) getRelativePath(folder) else folder
|
||||||
|
|
||||||
fun deleteFile(): Int {
|
fun deleteFile(): Int {
|
||||||
return delete(context, name, folder, extension, parentId)
|
return delete(context, name, relativePath, extension, parentId, basePath.first)
|
||||||
}
|
}
|
||||||
|
|
||||||
val stream = setupStream(context, name, folder, extension, tryResume)
|
val stream = setupStream(context, name, relativePath, extension, tryResume)
|
||||||
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
|
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
|
||||||
|
|
||||||
val resume = stream.resume!!
|
val resume = stream.resume!!
|
||||||
|
@ -681,7 +704,12 @@ object VideoDownloadManager {
|
||||||
context.setKey(
|
context.setKey(
|
||||||
KEY_DOWNLOAD_INFO,
|
KEY_DOWNLOAD_INFO,
|
||||||
it.toString(),
|
it.toString(),
|
||||||
DownloadedFileInfo(bytesTotal, relativePath, displayName)
|
DownloadedFileInfo(
|
||||||
|
bytesTotal,
|
||||||
|
relativePath ?: "",
|
||||||
|
displayName,
|
||||||
|
basePath = basePath.second
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -789,6 +817,7 @@ object VideoDownloadManager {
|
||||||
fileStream.write(buffer, 0, count)
|
fileStream.write(buffer, 0, count)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
isFailed = true
|
isFailed = true
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
@ -832,16 +861,118 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRelativePath(folder: String?): String {
|
|
||||||
return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
|
/**
|
||||||
|
* Guarantees a directory is present with the dir name (if createMissingDirectories is true).
|
||||||
|
* Works recursively when '/' is present.
|
||||||
|
* Will remove any file with the dir name if present and add directory.
|
||||||
|
* Will not work if the parent directory does not exist.
|
||||||
|
*
|
||||||
|
* @param directoryName if null will use the current path.
|
||||||
|
* @return UniFile / null if createMissingDirectories = false and folder is not found.
|
||||||
|
* */
|
||||||
|
private fun UniFile.gotoDir(directoryName: String?, createMissingDirectories: Boolean = true): UniFile? {
|
||||||
|
|
||||||
|
// May give this error on scoped storage.
|
||||||
|
// W/DocumentsContract: Failed to create document
|
||||||
|
// java.lang.IllegalArgumentException: Parent document isn't a directory
|
||||||
|
|
||||||
|
// Not present in latest testing.
|
||||||
|
|
||||||
|
// println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Creates itself from parent if doesn't exist.
|
||||||
|
if (!this.exists() && createMissingDirectories && !this.name.isNullOrBlank()) {
|
||||||
|
if (this.parentFile != null) {
|
||||||
|
this.parentFile?.createDirectory(this.name)
|
||||||
|
} else if (this.filePath != null) {
|
||||||
|
UniFile.fromFile(File(this.filePath!!).parentFile)?.createDirectory(this.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val allDirectories = directoryName?.split("/")
|
||||||
|
return if (allDirectories?.size == 1 || allDirectories == null) {
|
||||||
|
val found = this.findFile(directoryName)
|
||||||
|
when {
|
||||||
|
directoryName.isNullOrBlank() -> this
|
||||||
|
found?.isDirectory == true -> found
|
||||||
|
|
||||||
|
!createMissingDirectories -> null
|
||||||
|
// Below creates directories
|
||||||
|
found?.isFile == true -> {
|
||||||
|
found.delete()
|
||||||
|
this.createDirectory(directoryName)
|
||||||
|
}
|
||||||
|
this.isDirectory -> this.createDirectory(directoryName)
|
||||||
|
else -> this.parentFile?.createDirectory(directoryName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var currentDirectory = this
|
||||||
|
allDirectories.forEach {
|
||||||
|
// If the next directory is not found it returns the deepest directory possible.
|
||||||
|
val nextDir = currentDirectory.gotoDir(it, createMissingDirectories)
|
||||||
|
currentDirectory = nextDir ?: return null
|
||||||
|
}
|
||||||
|
currentDirectory
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayName(name: String, extension: String): String {
|
private fun getDisplayName(name: String, extension: String): String {
|
||||||
return "$name.$extension"
|
return "$name.$extension"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNormalPath(relativePath: String, displayName: String): String {
|
/**
|
||||||
return "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
|
* Gets the default download path as an UniFile.
|
||||||
|
* Vital for legacy downloads, be careful about changing anything here.
|
||||||
|
*
|
||||||
|
* As of writing UniFile is used for everything but download directory on scoped storage.
|
||||||
|
* Special ContentResolver fuckery is needed for that as UniFile doesn't work.
|
||||||
|
* */
|
||||||
|
fun getDownloadDir(): UniFile? {
|
||||||
|
// See https://www.py4u.net/discuss/614761
|
||||||
|
return UniFile.fromFile(
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory().absolutePath + File.separatorChar +
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("TODO fix UniFile to work with download directory.")
|
||||||
|
private fun getRelativePath(folder: String?): String {
|
||||||
|
return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a string to an UniFile. Used for stored string paths such as settings.
|
||||||
|
* Should only be used to get a download path.
|
||||||
|
* */
|
||||||
|
private fun basePathToFile(context: Context, path: String?): UniFile? {
|
||||||
|
return when {
|
||||||
|
path.isNullOrBlank() -> getDownloadDir()
|
||||||
|
path.startsWith("content://") -> UniFile.fromUri(context, path.toUri())
|
||||||
|
else -> UniFile.fromFile(File(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base path where downloaded things should be stored, changes depending on settings.
|
||||||
|
* Returns the file and a string to be stored for future file retrieval.
|
||||||
|
* UniFile.filePath is not sufficient for storage.
|
||||||
|
* */
|
||||||
|
fun Context.getBasePath(): Pair<UniFile?, String?> {
|
||||||
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val basePathSetting = settingsManager.getString(getString(R.string.download_path_key), null)
|
||||||
|
return basePathToFile(this, basePathSetting) to basePathSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UniFile?.isDownloadDir(): Boolean {
|
||||||
|
return this != null && this.filePath == getDownloadDir()?.filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun delete(
|
private fun delete(
|
||||||
|
@ -850,21 +981,30 @@ object VideoDownloadManager {
|
||||||
folder: String?,
|
folder: String?,
|
||||||
extension: String,
|
extension: String,
|
||||||
parentId: Int?,
|
parentId: Int?,
|
||||||
|
basePath: UniFile?
|
||||||
): Int {
|
): Int {
|
||||||
val relativePath = getRelativePath(folder)
|
|
||||||
val displayName = getDisplayName(name, extension)
|
val displayName = getDisplayName(name, extension)
|
||||||
|
|
||||||
if (isScopedStorage()) {
|
// If scoped storage and using download dir (not accessible with UniFile)
|
||||||
|
if (isScopedStorage && basePath.isDownloadDir()) {
|
||||||
|
val relativePath = getRelativePath(folder)
|
||||||
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
|
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
|
||||||
if (lastContent != null) {
|
if (lastContent != null) {
|
||||||
context.contentResolver.delete(lastContent, null, null)
|
context.contentResolver.delete(lastContent, null, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!File(getNormalPath(relativePath, displayName)).delete()) return ERROR_DELETING_FILE
|
val dir = basePath?.gotoDir(folder)
|
||||||
|
val file = dir?.findFile(displayName)
|
||||||
|
val success = file?.delete()
|
||||||
|
if (success != true) return ERROR_DELETING_FILE else {
|
||||||
|
// Cleans up empty directory
|
||||||
|
if (dir.listFiles()?.isEmpty() == true) dir.delete()
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
parentId?.let {
|
parentId?.let {
|
||||||
downloadDeleteEvent.invoke(parentId)
|
downloadDeleteEvent.invoke(parentId)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return SUCCESS_STOPPED
|
return SUCCESS_STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -898,16 +1038,20 @@ object VideoDownloadManager {
|
||||||
)
|
)
|
||||||
|
|
||||||
var realIndex = startIndex ?: 0
|
var realIndex = startIndex ?: 0
|
||||||
val stream = setupStream(context, name, folder, extension, realIndex > 0)
|
val basePath = context.getBasePath()
|
||||||
|
|
||||||
|
val relativePath = if (isScopedStorage && basePath.first.isDownloadDir()) getRelativePath(folder) else folder
|
||||||
|
|
||||||
|
val stream = setupStream(context, name, relativePath, extension, realIndex > 0)
|
||||||
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
|
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
|
||||||
|
|
||||||
if (!stream.resume!!) realIndex = 0
|
if (!stream.resume!!) realIndex = 0
|
||||||
val fileLengthAdd = stream.fileLength!!
|
val fileLengthAdd = stream.fileLength!!
|
||||||
val tsIterator = m3u8Helper.hlsYield(listOf(m3u8), realIndex)
|
val tsIterator = m3u8Helper.hlsYield(listOf(m3u8), realIndex)
|
||||||
|
|
||||||
val relativePath = getRelativePath(folder)
|
|
||||||
val displayName = getDisplayName(name, extension)
|
val displayName = getDisplayName(name, extension)
|
||||||
|
|
||||||
|
|
||||||
val fileStream = stream.fileStream!!
|
val fileStream = stream.fileStream!!
|
||||||
|
|
||||||
val firstTs = tsIterator.next()
|
val firstTs = tsIterator.next()
|
||||||
|
@ -920,7 +1064,7 @@ object VideoDownloadManager {
|
||||||
val totalTs = firstTs.totalTs.toLong()
|
val totalTs = firstTs.totalTs.toLong()
|
||||||
|
|
||||||
fun deleteFile(): Int {
|
fun deleteFile(): Int {
|
||||||
return delete(context, name, folder, extension, parentId)
|
return delete(context, name, relativePath, extension, parentId, basePath.first)
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Most of the auto generated m3u8 out there have TS of the same size.
|
Most of the auto generated m3u8 out there have TS of the same size.
|
||||||
|
@ -939,13 +1083,15 @@ object VideoDownloadManager {
|
||||||
it.toString(),
|
it.toString(),
|
||||||
DownloadedFileInfo(
|
DownloadedFileInfo(
|
||||||
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
|
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
|
||||||
relativePath,
|
relativePath ?: "",
|
||||||
displayName,
|
displayName,
|
||||||
tsProgress.toString()
|
tsProgress.toString(),
|
||||||
|
basePath = basePath.second
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInfo()
|
updateInfo()
|
||||||
|
|
||||||
fun updateNotification() {
|
fun updateNotification() {
|
||||||
|
@ -1153,7 +1299,6 @@ object VideoDownloadManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} ?: ERROR_UNKNOWN
|
} ?: ERROR_UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1189,7 +1334,7 @@ object VideoDownloadManager {
|
||||||
link,
|
link,
|
||||||
notificationCallback,
|
notificationCallback,
|
||||||
resume
|
resume
|
||||||
)
|
).also { println("Single episode finished with return code: $it") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (connectionResult != null && connectionResult > 0) { // SUCCESS
|
if (connectionResult != null && connectionResult > 0) { // SUCCESS
|
||||||
|
@ -1217,8 +1362,9 @@ object VideoDownloadManager {
|
||||||
|
|
||||||
private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? {
|
private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? {
|
||||||
val info = context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null
|
val info = context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null
|
||||||
|
val base = basePathToFile(context, info.basePath)
|
||||||
|
|
||||||
if (isScopedStorage()) {
|
if (isScopedStorage && base.isDownloadDir()) {
|
||||||
val cr = context.contentResolver ?: return null
|
val cr = context.contentResolver ?: return null
|
||||||
val fileUri =
|
val fileUri =
|
||||||
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) ?: return null
|
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) ?: return null
|
||||||
|
@ -1226,10 +1372,28 @@ object VideoDownloadManager {
|
||||||
if (fileLength == 0L) return null
|
if (fileLength == 0L) return null
|
||||||
return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri)
|
return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri)
|
||||||
} else {
|
} else {
|
||||||
val normalPath = getNormalPath(info.relativePath, info.displayName)
|
|
||||||
val dFile = File(normalPath)
|
val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName)
|
||||||
if (!dFile.exists()) return null
|
|
||||||
return DownloadedFileInfoResult(dFile.length(), info.totalBytes, dFile.toUri())
|
// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
|
||||||
|
// val dFile = File(normalPath)
|
||||||
|
|
||||||
|
if (file?.exists() != true) return null
|
||||||
|
|
||||||
|
return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the true download size as Scoped Storage sometimes wrongly returns 0.
|
||||||
|
* */
|
||||||
|
fun UniFile.size(): Long {
|
||||||
|
val len = length()
|
||||||
|
return if (len <= 1) {
|
||||||
|
val inputStream = this.openInputStream()
|
||||||
|
return inputStream.available().toLong().also { inputStream.closeQuietly() }
|
||||||
|
} else {
|
||||||
|
len
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1245,8 +1409,8 @@ object VideoDownloadManager {
|
||||||
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
||||||
downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped))
|
downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped))
|
||||||
downloadDeleteEvent.invoke(id)
|
downloadDeleteEvent.invoke(id)
|
||||||
|
val base = basePathToFile(context, info.basePath)
|
||||||
if (isScopedStorage()) {
|
if (isScopedStorage && base.isDownloadDir()) {
|
||||||
val cr = context.contentResolver ?: return false
|
val cr = context.contentResolver ?: return false
|
||||||
val fileUri =
|
val fileUri =
|
||||||
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName)
|
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName)
|
||||||
|
@ -1254,10 +1418,17 @@ object VideoDownloadManager {
|
||||||
|
|
||||||
return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0
|
return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0
|
||||||
} else {
|
} else {
|
||||||
val normalPath = getNormalPath(info.relativePath, info.displayName)
|
val file = base?.gotoDir(info.relativePath)?.findFile(info.displayName)
|
||||||
val dFile = File(normalPath)
|
// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
|
||||||
if (!dFile.exists()) return true
|
// val dFile = File(normalPath)
|
||||||
return dFile.delete()
|
if (file?.exists() != true) return true
|
||||||
|
return try {
|
||||||
|
file.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
val cr = context.contentResolver
|
||||||
|
cr.delete(file.uri, null, null) > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1272,19 +1443,19 @@ object VideoDownloadManager {
|
||||||
setKey: Boolean = true
|
setKey: Boolean = true
|
||||||
) {
|
) {
|
||||||
if (!currentDownloads.any { it == pkg.item.ep.id }) {
|
if (!currentDownloads.any { it == pkg.item.ep.id }) {
|
||||||
if (currentDownloads.size == maxConcurrentDownloads) {
|
// if (currentDownloads.size == maxConcurrentDownloads) {
|
||||||
main {
|
// main {
|
||||||
// showToast( // can be replaced with regular Toast
|
//// showToast( // can be replaced with regular Toast
|
||||||
// context,
|
//// context,
|
||||||
// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${
|
//// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${
|
||||||
// context.getString(
|
//// context.getString(
|
||||||
// R.string.queued
|
//// R.string.queued
|
||||||
// )
|
//// )
|
||||||
// }",
|
//// }",
|
||||||
// Toast.LENGTH_SHORT
|
//// Toast.LENGTH_SHORT
|
||||||
// )
|
//// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
downloadQueue.addLast(pkg)
|
downloadQueue.addLast(pkg)
|
||||||
downloadCheck(context, notificationCallback)
|
downloadCheck(context, notificationCallback)
|
||||||
if (setKey) saveQueue(context)
|
if (setKey) saveQueue(context)
|
||||||
|
|
|
@ -24,11 +24,14 @@
|
||||||
<string name="show_fillers_key" translatable="false">show_fillers_key</string>
|
<string name="show_fillers_key" translatable="false">show_fillers_key</string>
|
||||||
<string name="provider_lang_key" translatable="false">provider_lang_key</string>
|
<string name="provider_lang_key" translatable="false">provider_lang_key</string>
|
||||||
<string name="dns_key" translatable="false">dns_key</string>
|
<string name="dns_key" translatable="false">dns_key</string>
|
||||||
|
<string name="download_path_key" translatable="false">download_path_key</string>
|
||||||
|
<string name="app_name_download_path" translatable="false">Cloudstream</string>
|
||||||
<string name="app_layout_key" translatable="false">app_layout_key</string>
|
<string name="app_layout_key" translatable="false">app_layout_key</string>
|
||||||
<string name="primary_color_key" translatable="false">primary_color_key</string>
|
<string name="primary_color_key" translatable="false">primary_color_key</string>
|
||||||
<string name="prefer_media_type_key" translatable="false">prefer_media_type_key</string>
|
<string name="prefer_media_type_key" translatable="false">prefer_media_type_key</string>
|
||||||
<string name="app_theme_key" translatable="false">app_theme_key</string>
|
<string name="app_theme_key" translatable="false">app_theme_key</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||||
<string name="extra_info_format" translatable="false" formatted="true">%d %s | %sMB</string>
|
<string name="extra_info_format" translatable="false" formatted="true">%d %s | %sMB</string>
|
||||||
<string name="storage_size_format" translatable="false" formatted="true">%s • %sGB</string>
|
<string name="storage_size_format" translatable="false" formatted="true">%s • %sGB</string>
|
||||||
|
@ -270,6 +273,7 @@
|
||||||
<string name="dns_pref">DNS over HTTPS</string>
|
<string name="dns_pref">DNS over HTTPS</string>
|
||||||
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
|
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
|
||||||
|
|
||||||
|
<string name="download_path_pref">Download path</string>
|
||||||
|
|
||||||
<string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string>
|
<string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string>
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,11 @@
|
||||||
android:summary="@string/dns_pref_summary"
|
android:summary="@string/dns_pref_summary"
|
||||||
android:icon="@drawable/ic_baseline_dns_24">
|
android:icon="@drawable/ic_baseline_dns_24">
|
||||||
</Preference>
|
</Preference>
|
||||||
|
<Preference
|
||||||
|
android:key="@string/download_path_key"
|
||||||
|
android:title="@string/download_path_pref"
|
||||||
|
android:icon="@drawable/netflix_download">
|
||||||
|
</Preference>
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_baseline_tv_24"
|
android:icon="@drawable/ic_baseline_tv_24"
|
||||||
android:key="@string/app_layout_key"
|
android:key="@string/app_layout_key"
|
||||||
|
|
Loading…
Reference in a new issue