forked from recloudstream/cloudstream
Unifile implementation
This commit is contained in:
parent
70c14d116c
commit
4b1e5ab9b1
5 changed files with 292 additions and 76 deletions
|
@ -85,14 +85,14 @@ dependencies {
|
|||
testImplementation 'org.json:json:20180813'
|
||||
|
||||
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 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-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-viewmodel-ktx:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
@ -141,10 +141,13 @@ dependencies {
|
|||
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
|
||||
|
||||
// Downloading
|
||||
implementation "androidx.work:work-runtime:2.7.0-rc01"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.0-rc01"
|
||||
implementation "androidx.work:work-runtime:2.7.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.0"
|
||||
|
||||
// Networking
|
||||
implementation "com.squareup.okhttp3:okhttp: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"
|
||||
}
|
|
@ -1,16 +1,22 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.restrictedApis
|
||||
import com.lagradost.cloudstream3.AcraApplication
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.setLocale
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.showToast
|
||||
|
@ -26,12 +32,41 @@ 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.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
|
||||
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
private var beneneCount = 0
|
||||
|
||||
// Open file picker
|
||||
private val pathPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
val 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
|
||||
private val languages = arrayListOf(
|
||||
Triple("\uD83C\uDDEA\uD83C\uDDF8", "Spanish", "es"),
|
||||
|
@ -61,6 +96,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!!
|
||||
val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!!
|
||||
val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!!
|
||||
val downloadPathPreference = findPreference<Preference>(getString(R.string.download_path_key))!!
|
||||
|
||||
legalPreference.setOnPreferenceClickListener {
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(it.context)
|
||||
|
@ -141,6 +177,48 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
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
|
||||
}
|
||||
|
||||
watchQualityPreference.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.quality_pref)
|
||||
val prefValues = resources.getIntArray(R.array.quality_pref_values)
|
||||
|
|
|
@ -15,11 +15,14 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.google.android.exoplayer2.util.UriUtil
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -126,7 +129,8 @@ object VideoDownloadManager {
|
|||
val totalBytes: Long,
|
||||
val relativePath: 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(
|
||||
|
@ -437,21 +441,29 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for getting video player subs.
|
||||
* @return List of pairs for the files in this format: <Name, Uri>
|
||||
* */
|
||||
fun getFolder(context: Context, relativePath: String): List<Pair<String, Uri>>? {
|
||||
if (isScopedStorage()) {
|
||||
val base = context.getBasePath().first
|
||||
val folder = base?.gotoDir(relativePath, false)
|
||||
|
||||
if (isScopedStorage && base.isDownloadDir()) {
|
||||
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()) }
|
||||
// val normalPath =
|
||||
// "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace(
|
||||
// '/',
|
||||
// File.separatorChar
|
||||
// )
|
||||
// val folder = File(normalPath)
|
||||
if (folder?.isDirectory == true) {
|
||||
return folder.listFiles()?.map { Pair(it.name ?: "", it.uri) }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
|
@ -501,9 +513,9 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
private fun isScopedStorage(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
val isScopedStorage: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
|
||||
|
||||
data class CreateNotificationMetadata(
|
||||
val type: DownloadType,
|
||||
|
@ -518,6 +530,56 @@ object VideoDownloadManager {
|
|||
val fileStream: OutputStream? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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? {
|
||||
|
||||
// Can give this error on scoped storage, haven't solved.
|
||||
// W/DocumentsContract: Failed to create document
|
||||
// java.lang.IllegalArgumentException: Parent document isn't a directory
|
||||
|
||||
try {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the appropriate file and creates a data stream from the file.
|
||||
* Used for initializing downloads.
|
||||
* */
|
||||
private fun setupStream(
|
||||
context: Context,
|
||||
name: String,
|
||||
|
@ -525,12 +587,14 @@ object VideoDownloadManager {
|
|||
extension: String,
|
||||
tryResume: Boolean,
|
||||
): StreamData {
|
||||
val relativePath = getRelativePath(folder)
|
||||
val displayName = getDisplayName(name, extension)
|
||||
val fileStream: OutputStream
|
||||
val fileLength: Long
|
||||
var resume = tryResume
|
||||
if (isScopedStorage()) {
|
||||
val baseFile = context.getBasePath()
|
||||
|
||||
if (isScopedStorage && baseFile.first?.isDownloadDir() == true) {
|
||||
val relativePath = getRelativePath(folder)
|
||||
val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||
|
||||
val currentExistingFile =
|
||||
|
@ -578,26 +642,25 @@ object VideoDownloadManager {
|
|||
fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else ""))
|
||||
?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||
} else {
|
||||
val normalPath = getNormalPath(relativePath, displayName)
|
||||
// NORMAL NON SCOPED STORAGE FILE CREATION
|
||||
val rFile = File(normalPath)
|
||||
if (!rFile.exists()) {
|
||||
println("SEYUP FFFFFFF")
|
||||
val subDir = baseFile.first?.gotoDir(folder)
|
||||
val rFile = subDir?.findFile(displayName)
|
||||
if (rFile?.exists() != true) {
|
||||
fileLength = 0
|
||||
rFile.parentFile?.mkdirs()
|
||||
if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||
if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||
} else {
|
||||
if (resume) {
|
||||
fileLength = rFile.length()
|
||||
} else {
|
||||
fileLength = 0
|
||||
rFile.parentFile?.mkdirs()
|
||||
if (!rFile.delete()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||
if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
|
||||
if (subDir.createFile(displayName) == null) 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
|
||||
}
|
||||
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
|
||||
}
|
||||
|
||||
|
@ -615,12 +678,14 @@ object VideoDownloadManager {
|
|||
return ERROR_UNKNOWN
|
||||
}
|
||||
|
||||
val relativePath = getRelativePath(folder)
|
||||
val basePath = context.getBasePath()
|
||||
|
||||
// val relativePath = getRelativePath(folder)
|
||||
val displayName = getDisplayName(name, extension)
|
||||
|
||||
|
||||
fun deleteFile(): Int {
|
||||
return delete(context, name, folder, extension, parentId)
|
||||
return delete(context, name, folder, extension, parentId, basePath.first)
|
||||
}
|
||||
|
||||
val stream = setupStream(context, name, folder, extension, tryResume)
|
||||
|
@ -681,7 +746,7 @@ object VideoDownloadManager {
|
|||
context.setKey(
|
||||
KEY_DOWNLOAD_INFO,
|
||||
it.toString(),
|
||||
DownloadedFileInfo(bytesTotal, relativePath, displayName)
|
||||
DownloadedFileInfo(bytesTotal, folder ?: "", displayName, basePath = basePath.second)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -832,16 +897,58 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getRelativePath(folder: String?): String {
|
||||
return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
|
||||
}
|
||||
|
||||
private fun getDisplayName(name: String, extension: String): String {
|
||||
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 == null -> 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 {
|
||||
println("DOWNLOAD DIR $this ${this?.filePath} ${this?.uri}")
|
||||
return this != null && this.filePath == getDownloadDir()?.filePath
|
||||
}
|
||||
|
||||
private fun delete(
|
||||
|
@ -850,21 +957,30 @@ object VideoDownloadManager {
|
|||
folder: String?,
|
||||
extension: String,
|
||||
parentId: Int?,
|
||||
basePath: UniFile?
|
||||
): Int {
|
||||
val relativePath = getRelativePath(folder)
|
||||
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)
|
||||
if (lastContent != null) {
|
||||
context.contentResolver.delete(lastContent, null, null)
|
||||
}
|
||||
} else {
|
||||
if (!File(getNormalPath(relativePath, displayName)).delete()) return ERROR_DELETING_FILE
|
||||
val dir = basePath?.gotoDir(folder)
|
||||
val success = dir?.findFile(displayName)
|
||||
?.delete()
|
||||
if (success != true) return ERROR_DELETING_FILE else {
|
||||
// Cleans up empty directory
|
||||
if (dir.listFiles()?.isEmpty() == true) dir.delete()
|
||||
}
|
||||
// }
|
||||
parentId?.let {
|
||||
downloadDeleteEvent.invoke(parentId)
|
||||
}
|
||||
}
|
||||
return SUCCESS_STOPPED
|
||||
}
|
||||
|
||||
|
@ -875,7 +991,7 @@ object VideoDownloadManager {
|
|||
folder: String?,
|
||||
parentId: Int?,
|
||||
startIndex: Int?,
|
||||
createNotificationCallback: (CreateNotificationMetadata) -> Unit
|
||||
createNotificationCallback: (VideoDownloadManager.CreateNotificationMetadata) -> Unit
|
||||
): Int {
|
||||
val extension = "mp4"
|
||||
fun logcatPrint(vararg items: Any?) {
|
||||
|
@ -905,9 +1021,11 @@ object VideoDownloadManager {
|
|||
val fileLengthAdd = stream.fileLength!!
|
||||
val tsIterator = m3u8Helper.hlsYield(listOf(m3u8), realIndex)
|
||||
|
||||
val relativePath = getRelativePath(folder)
|
||||
// val relativePath = getRelativePath(folder)
|
||||
val displayName = getDisplayName(name, extension)
|
||||
|
||||
val basePath = context.getBasePath()
|
||||
|
||||
val fileStream = stream.fileStream!!
|
||||
|
||||
val firstTs = tsIterator.next()
|
||||
|
@ -920,7 +1038,7 @@ object VideoDownloadManager {
|
|||
val totalTs = firstTs.totalTs.toLong()
|
||||
|
||||
fun deleteFile(): Int {
|
||||
return delete(context, name, folder, extension, parentId)
|
||||
return delete(context, name, folder, extension, parentId, basePath.first)
|
||||
}
|
||||
/*
|
||||
Most of the auto generated m3u8 out there have TS of the same size.
|
||||
|
@ -939,13 +1057,15 @@ object VideoDownloadManager {
|
|||
it.toString(),
|
||||
DownloadedFileInfo(
|
||||
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
|
||||
relativePath,
|
||||
folder ?: "",
|
||||
displayName,
|
||||
tsProgress.toString()
|
||||
tsProgress.toString(),
|
||||
basePath = basePath.second
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updateInfo()
|
||||
|
||||
fun updateNotification() {
|
||||
|
@ -1217,8 +1337,9 @@ object VideoDownloadManager {
|
|||
|
||||
private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? {
|
||||
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 fileUri =
|
||||
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) ?: return null
|
||||
|
@ -1226,10 +1347,12 @@ object VideoDownloadManager {
|
|||
if (fileLength == 0L) return null
|
||||
return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri)
|
||||
} else {
|
||||
val normalPath = getNormalPath(info.relativePath, info.displayName)
|
||||
val dFile = File(normalPath)
|
||||
if (!dFile.exists()) return null
|
||||
return DownloadedFileInfoResult(dFile.length(), info.totalBytes, dFile.toUri())
|
||||
val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName)
|
||||
// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
|
||||
// val dFile = File(normalPath)
|
||||
if (file?.exists() != true) return null
|
||||
|
||||
return DownloadedFileInfoResult(file.length(), info.totalBytes, file.uri)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1245,8 +1368,9 @@ object VideoDownloadManager {
|
|||
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
||||
downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped))
|
||||
downloadDeleteEvent.invoke(id)
|
||||
val base = basePathToFile(context, info.basePath)
|
||||
|
||||
if (isScopedStorage()) {
|
||||
if (isScopedStorage && base.isDownloadDir()) {
|
||||
val cr = context.contentResolver ?: return false
|
||||
val fileUri =
|
||||
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName)
|
||||
|
@ -1254,10 +1378,11 @@ object VideoDownloadManager {
|
|||
|
||||
return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0
|
||||
} else {
|
||||
val normalPath = getNormalPath(info.relativePath, info.displayName)
|
||||
val dFile = File(normalPath)
|
||||
if (!dFile.exists()) return true
|
||||
return dFile.delete()
|
||||
val file = base?.gotoDir(info.relativePath)?.findFile(info.displayName)
|
||||
// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
|
||||
// val dFile = File(normalPath)
|
||||
if (file?.exists() != true) return true
|
||||
return file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1272,19 +1397,19 @@ object VideoDownloadManager {
|
|||
setKey: Boolean = true
|
||||
) {
|
||||
if (!currentDownloads.any { it == pkg.item.ep.id }) {
|
||||
if (currentDownloads.size == maxConcurrentDownloads) {
|
||||
main {
|
||||
// showToast( // can be replaced with regular Toast
|
||||
// context,
|
||||
// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${
|
||||
// context.getString(
|
||||
// R.string.queued
|
||||
// )
|
||||
// }",
|
||||
// Toast.LENGTH_SHORT
|
||||
// )
|
||||
}
|
||||
}
|
||||
// if (currentDownloads.size == maxConcurrentDownloads) {
|
||||
// main {
|
||||
//// showToast( // can be replaced with regular Toast
|
||||
//// context,
|
||||
//// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${
|
||||
//// context.getString(
|
||||
//// R.string.queued
|
||||
//// )
|
||||
//// }",
|
||||
//// Toast.LENGTH_SHORT
|
||||
//// )
|
||||
// }
|
||||
// }
|
||||
downloadQueue.addLast(pkg)
|
||||
downloadCheck(context, notificationCallback)
|
||||
if (setKey) saveQueue(context)
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
<string name="show_fillers_key" translatable="false">show_fillers_key</string>
|
||||
<string name="provider_lang_key" translatable="false">provider_lang_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>
|
||||
|
||||
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
|
@ -267,6 +271,7 @@
|
|||
<string name="dns_pref">DNS over HTTPS</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>
|
||||
|
||||
|
|
|
@ -99,6 +99,11 @@
|
|||
android:summary="@string/dns_pref_summary"
|
||||
android:icon="@drawable/ic_baseline_dns_24">
|
||||
</Preference>
|
||||
<Preference
|
||||
android:key="@string/download_path_key"
|
||||
android:title="@string/download_path_pref"
|
||||
android:icon="@drawable/netflix_download">
|
||||
</Preference>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="search"
|
||||
|
|
Loading…
Reference in a new issue