Unifile implementation

This commit is contained in:
Blatzar 2021-11-01 16:33:46 +01:00
parent 70c14d116c
commit 4b1e5ab9b1
5 changed files with 292 additions and 76 deletions

View file

@ -85,14 +85,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'
@ -141,10 +141,13 @@ dependencies {
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0' //implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
// Downloading // Downloading
implementation "androidx.work:work-runtime:2.7.0-rc01" implementation "androidx.work:work-runtime:2.7.0"
implementation "androidx.work:work-runtime-ktx:2.7.0-rc01" implementation "androidx.work:work-runtime-ktx:2.7.0"
// 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"
} }

View file

@ -1,16 +1,22 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.content.Intent
import android.net.Uri
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
@ -26,12 +32,41 @@ 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
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : PreferenceFragmentCompat() {
private var beneneCount = 0 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 // 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"),
@ -61,6 +96,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))!!
legalPreference.setOnPreferenceClickListener { legalPreference.setOnPreferenceClickListener {
val builder: AlertDialog.Builder = AlertDialog.Builder(it.context) val builder: AlertDialog.Builder = AlertDialog.Builder(it.context)
@ -141,6 +177,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
}
watchQualityPreference.setOnPreferenceClickListener { watchQualityPreference.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.quality_pref) val prefNames = resources.getStringArray(R.array.quality_pref)
val prefValues = resources.getIntArray(R.array.quality_pref_values) val prefValues = resources.getIntArray(R.array.quality_pref_values)

View file

@ -15,11 +15,14 @@ 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.google.android.exoplayer2.util.UriUtil
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
@ -126,7 +129,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(
@ -256,9 +260,9 @@ object VideoDownloadManager {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
val pendingIntent: PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val pendingIntent: PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else { } else {
PendingIntent.getActivity(context, 0, intent, 0) PendingIntent.getActivity(context, 0, intent, 0)
} }
builder.setContentIntent(pendingIntent) builder.setContentIntent(pendingIntent)
} }
@ -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>>? { 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) 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)
@ -501,9 +513,9 @@ object VideoDownloadManager {
} }
} }
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 +530,56 @@ object VideoDownloadManager {
val fileStream: OutputStream? = null, 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( private fun setupStream(
context: Context, context: Context,
name: String, name: String,
@ -525,12 +587,14 @@ 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 relativePath = getRelativePath(folder)
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 =
@ -578,26 +642,25 @@ 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) println("SEYUP FFFFFFF")
// NORMAL NON SCOPED STORAGE FILE CREATION val subDir = baseFile.first?.gotoDir(folder)
val rFile = File(normalPath) val rFile = subDir?.findFile(displayName)
if (!rFile.exists()) { if (rFile?.exists() != true) {
fileLength = 0 fileLength = 0
rFile.parentFile?.mkdirs() if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
} else { } else {
if (resume) { if (resume) {
fileLength = rFile.length() fileLength = rFile.length()
} else { } else {
fileLength = 0 fileLength = 0
rFile.parentFile?.mkdirs()
if (!rFile.delete()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) 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
} }
if (fileLength == 0L) resume = false
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
} }
@ -615,12 +678,14 @@ object VideoDownloadManager {
return ERROR_UNKNOWN return ERROR_UNKNOWN
} }
val relativePath = getRelativePath(folder) val basePath = context.getBasePath()
// val relativePath = getRelativePath(folder)
val displayName = getDisplayName(name, extension) val displayName = getDisplayName(name, extension)
fun deleteFile(): Int { 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) val stream = setupStream(context, name, folder, extension, tryResume)
@ -681,7 +746,7 @@ object VideoDownloadManager {
context.setKey( context.setKey(
KEY_DOWNLOAD_INFO, KEY_DOWNLOAD_INFO,
it.toString(), 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 { 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 == 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( private fun delete(
@ -850,20 +957,29 @@ 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 success = dir?.findFile(displayName)
parentId?.let { ?.delete()
downloadDeleteEvent.invoke(parentId) 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 return SUCCESS_STOPPED
} }
@ -875,7 +991,7 @@ object VideoDownloadManager {
folder: String?, folder: String?,
parentId: Int?, parentId: Int?,
startIndex: Int?, startIndex: Int?,
createNotificationCallback: (CreateNotificationMetadata) -> Unit createNotificationCallback: (VideoDownloadManager.CreateNotificationMetadata) -> Unit
): Int { ): Int {
val extension = "mp4" val extension = "mp4"
fun logcatPrint(vararg items: Any?) { fun logcatPrint(vararg items: Any?) {
@ -905,9 +1021,11 @@ object VideoDownloadManager {
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 relativePath = getRelativePath(folder)
val displayName = getDisplayName(name, extension) val displayName = getDisplayName(name, extension)
val basePath = context.getBasePath()
val fileStream = stream.fileStream!! val fileStream = stream.fileStream!!
val firstTs = tsIterator.next() val firstTs = tsIterator.next()
@ -920,7 +1038,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, folder, 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 +1057,15 @@ object VideoDownloadManager {
it.toString(), it.toString(),
DownloadedFileInfo( DownloadedFileInfo(
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
relativePath, folder ?: "",
displayName, displayName,
tsProgress.toString() tsProgress.toString(),
basePath = basePath.second
) )
) )
} }
} }
updateInfo() updateInfo()
fun updateNotification() { fun updateNotification() {
@ -1217,8 +1337,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 +1347,12 @@ 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 file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName)
val dFile = File(normalPath) // val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
if (!dFile.exists()) return null // val dFile = File(normalPath)
return DownloadedFileInfoResult(dFile.length(), info.totalBytes, dFile.toUri()) 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)) 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 +1378,11 @@ 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 file.delete()
} }
} }
@ -1272,19 +1397,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)

View file

@ -24,6 +24,10 @@
<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>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG --> <!-- 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">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>

View file

@ -99,6 +99,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>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="search" android:key="search"