mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
bump + refactor
This commit is contained in:
parent
b38a9b1ff5
commit
8193e39b30
11 changed files with 40 additions and 779 deletions
|
@ -32,11 +32,12 @@ android {
|
|||
enable = true
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
// disable this for now
|
||||
//externalNativeBuild {
|
||||
// cmake {
|
||||
// path("CMakeLists.txt")
|
||||
// }
|
||||
//}
|
||||
|
||||
signingConfigs {
|
||||
create("prerelease") {
|
||||
|
@ -58,7 +59,7 @@ android {
|
|||
targetSdk = 29
|
||||
|
||||
versionCode = 59
|
||||
versionName = "4.1.7"
|
||||
versionName = "4.1.8"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
|
@ -232,7 +233,7 @@ dependencies {
|
|||
// To fix SSL fuckery on android 9
|
||||
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
||||
// Util to skip the URI file fuckery 🙏
|
||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||
implementation("com.github.LagradOst:SafeFile:0.0.2")
|
||||
|
||||
// API because cba maintaining it myself
|
||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
||||
|
|
|
@ -144,6 +144,7 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
|||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.safefile.SafeFile
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
|
@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
companion object {
|
||||
const val TAG = "MAINACT"
|
||||
var lastError: String? = null
|
||||
|
||||
/**
|
||||
* Setting this will automatically enter the query in the search
|
||||
* next time the search fragment is opened.
|
||||
|
@ -366,7 +368,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
nextSearchQuery =
|
||||
try {
|
||||
URLDecoder.decode(query, "UTF-8")
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
query
|
||||
}
|
||||
|
@ -859,7 +861,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
RecyclerView::class.java.declaredMethods.firstOrNull {
|
||||
it.name == "scrollStep"
|
||||
}?.also { it.isAccessible = true }
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -906,11 +908,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (dx > 0) dx else 0
|
||||
}
|
||||
|
||||
if(!NO_MOVE_LIST) {
|
||||
if (!NO_MOVE_LIST) {
|
||||
parent.smoothScrollBy(rdx, 0)
|
||||
}else {
|
||||
} else {
|
||||
val smoothScroll = reflectedScroll
|
||||
if(smoothScroll == null) {
|
||||
if (smoothScroll == null) {
|
||||
parent.smoothScrollBy(rdx, 0)
|
||||
} else {
|
||||
try {
|
||||
|
@ -920,12 +922,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
val out = IntArray(2)
|
||||
smoothScroll.invoke(parent, rdx, 0, out)
|
||||
val scrolledX = out[0]
|
||||
if(abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
|
||||
if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
|
||||
smoothScroll.invoke(parent, -rdx, 0, out)
|
||||
parent.smoothScrollBy(scrolledX, 0)
|
||||
if (NO_MOVE_LIST) targetDx = scrolledX
|
||||
}
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
parent.smoothScrollBy(rdx, 0)
|
||||
}
|
||||
}
|
||||
|
@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
snackbar.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ioSafe { SafeFile.check(this@MainActivity) }
|
||||
|
||||
if (PluginManager.checkSafeModeFile()) {
|
||||
normalSafeApiCall {
|
||||
|
|
|
@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
object NativeCrashHandler {
|
||||
// external fun triggerNativeCrash()
|
||||
private external fun initNativeCrashHandler()
|
||||
/*private external fun initNativeCrashHandler()
|
||||
private external fun getSignalStatus(): Int
|
||||
|
||||
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
||||
|
@ -49,5 +49,5 @@ object NativeCrashHandler {
|
|||
}
|
||||
|
||||
initSignalPolling()
|
||||
}
|
||||
}*/
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
|
@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity
|
|||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.SafeFile
|
||||
|
||||
const val DTAG = "PlayerActivity"
|
||||
|
||||
|
@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
|||
listOf(
|
||||
ExtractorUri(
|
||||
uri = uri,
|
||||
name = name ?: getString(R.string.downloaded_file)
|
||||
name = name ?: getString(R.string.downloaded_file),
|
||||
// well not the same as a normal id, but we take it as users may want to
|
||||
// play downloaded files and save the location
|
||||
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.preference.PreferenceManager
|
||||
import androidx.media3.common.Format.NO_VALUE
|
||||
import androidx.media3.common.MimeTypes
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
|
@ -52,7 +50,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.SafeFile
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
return durPos.position
|
||||
}
|
||||
|
||||
var currentVerifyLink: Job? = null
|
||||
private var currentVerifyLink: Job? = null
|
||||
|
||||
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
|
||||
currentVerifyLink?.cancel()
|
||||
|
|
|
@ -38,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
|||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.SafeFile
|
||||
|
||||
fun getCurrentLocale(context: Context): String {
|
||||
val res = context.resources
|
||||
|
@ -335,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
|
||||
val currentDir =
|
||||
settingsManager.getString(getString(R.string.download_path_pref), null)
|
||||
?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() }
|
||||
?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx)?.filePath() }
|
||||
|
||||
activity?.showBottomDialog(
|
||||
dirs + listOf("Custom"),
|
||||
|
|
|
@ -158,6 +158,7 @@ object BackupUtils {
|
|||
val displayName = "CS3_Backup_${date}"
|
||||
val backupFile = getBackup()
|
||||
val stream = setupStream(this, displayName, null, ext, false)
|
||||
|
||||
fileStream = stream.openNew()
|
||||
printStream = PrintWriter(fileStream)
|
||||
printStream.print(mapper.writeValueAsString(backupFile))
|
||||
|
|
|
@ -35,8 +35,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
|
|||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.storage.MediaFileContentType
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.MediaFileContentType
|
||||
import com.lagradost.safefile.SafeFile
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -554,9 +554,8 @@ object VideoDownloadManager {
|
|||
extension: String,
|
||||
tryResume: Boolean,
|
||||
): StreamData {
|
||||
val (base, _) = context.getBasePath()
|
||||
return setupStream(
|
||||
base ?: throw IOException("Bad config"),
|
||||
context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"),
|
||||
name,
|
||||
folder,
|
||||
extension,
|
||||
|
@ -1401,7 +1400,12 @@ object VideoDownloadManager {
|
|||
metadata.type = DownloadType.IsFailed
|
||||
}
|
||||
} finally {
|
||||
fileMutex.unlock()
|
||||
try {
|
||||
// may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling
|
||||
fileMutex.unlock()
|
||||
} catch (t : Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,389 +0,0 @@
|
|||
package com.lagradost.cloudstream3.utils.storage
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hippo.unifile.UniRandomAccessFile
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import okhttp3.internal.closeQuietly
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
||||
enum class MediaFileContentType {
|
||||
Downloads,
|
||||
Audio,
|
||||
Video,
|
||||
Images,
|
||||
}
|
||||
|
||||
// https://developer.android.com/training/data-storage/shared/media
|
||||
fun MediaFileContentType.toPath(): String {
|
||||
return when (this) {
|
||||
MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS
|
||||
MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC
|
||||
MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES
|
||||
MediaFileContentType.Images -> Environment.DIRECTORY_DCIM
|
||||
}
|
||||
}
|
||||
|
||||
fun MediaFileContentType.defaultPrefix(): String {
|
||||
return Environment.getExternalStorageDirectory().absolutePath
|
||||
}
|
||||
|
||||
fun MediaFileContentType.toAbsolutePath(): String {
|
||||
return defaultPrefix() + File.separator +
|
||||
this.toPath()
|
||||
}
|
||||
|
||||
fun replaceDuplicateFileSeparators(path: String): String {
|
||||
return path.replace(Regex("${File.separator}+"), File.separator)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun MediaFileContentType.toUri(external: Boolean): Uri {
|
||||
val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL
|
||||
return when (this) {
|
||||
MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume)
|
||||
MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume)
|
||||
MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume)
|
||||
MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
class MediaFile(
|
||||
private val context: Context,
|
||||
private val folderType: MediaFileContentType,
|
||||
private val external: Boolean = true,
|
||||
absolutePath: String,
|
||||
) : SafeFile {
|
||||
override fun toString(): String {
|
||||
return sanitizedAbsolutePath
|
||||
}
|
||||
|
||||
// this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt"
|
||||
private val sanitizedAbsolutePath: String =
|
||||
replaceDuplicateFileSeparators(absolutePath)
|
||||
|
||||
// this is only a directory if the filepath ends with a /
|
||||
private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator)
|
||||
private val isFile: Boolean = !isDir
|
||||
|
||||
// this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello"
|
||||
private val relativePath: String =
|
||||
replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast(
|
||||
File.separator
|
||||
)
|
||||
|
||||
// "/hello/text.txt" => "text.txt"
|
||||
private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator)
|
||||
private val baseUri = folderType.toUri(external)
|
||||
private val contentResolver: ContentResolver = context.contentResolver
|
||||
|
||||
init {
|
||||
// some standard asserts that should always be hold or else this class wont work
|
||||
assert(!relativePath.endsWith(File.separator))
|
||||
assert(!(isDir && isFile))
|
||||
assert(!relativePath.contains(File.separator + File.separator))
|
||||
assert(!namePath.contains(File.separator))
|
||||
|
||||
if (isDir) {
|
||||
assert(namePath.isBlank())
|
||||
} else {
|
||||
assert(namePath.isNotBlank())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun splitFilenameExt(name: String): Pair<String, String?> {
|
||||
val split = name.indexOfLast { it == '.' }
|
||||
if (split <= 0) return name to null
|
||||
val ext = name.substring(split + 1 until name.length)
|
||||
if (ext.isBlank()) return name to null
|
||||
|
||||
return name.substring(0 until split) to ext
|
||||
}
|
||||
|
||||
private fun splitFilenameMime(name: String): Pair<String, String?> {
|
||||
val (display, ext) = splitFilenameExt(name)
|
||||
val mimeType = when (ext) {
|
||||
|
||||
// Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents
|
||||
// downloading to /Downloads yet it works with null
|
||||
|
||||
"vtt" -> null // "text/vtt"
|
||||
"mp4" -> "video/mp4"
|
||||
"srt" -> null // "application/x-subrip"//"text/plain"
|
||||
else -> null
|
||||
}
|
||||
return display to mimeType
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendRelativePath(path: String, folder: Boolean): MediaFile? {
|
||||
if (isFile) return null
|
||||
|
||||
// VideoDownloadManager.sanitizeFilename(path.replace(File.separator, ""))
|
||||
|
||||
// in case of duplicate path, aka Download -> Download
|
||||
if (relativePath == path) return this
|
||||
|
||||
val newPath =
|
||||
sanitizedAbsolutePath + path + if (folder) File.separator else ""
|
||||
|
||||
return MediaFile(
|
||||
context = context,
|
||||
folderType = folderType,
|
||||
external = external,
|
||||
absolutePath = newPath
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUri(displayName: String? = namePath): Uri? {
|
||||
if (displayName == null) return null
|
||||
if (isFile) return null
|
||||
val (name, mime) = splitFilenameMime(displayName)
|
||||
|
||||
val newFile = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.MediaColumns.TITLE, name)
|
||||
if (mime != null)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, mime)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
}
|
||||
return contentResolver.insert(baseUri, newFile)
|
||||
}
|
||||
|
||||
override fun createFile(displayName: String?): SafeFile? {
|
||||
if (isFile || displayName == null) return null
|
||||
query(displayName)?.uri ?: createUri(displayName) ?: return null
|
||||
return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null)
|
||||
}
|
||||
|
||||
override fun createDirectory(directoryName: String?): SafeFile? {
|
||||
if (directoryName == null) return null
|
||||
// we don't create a dir here tbh, just fake create it
|
||||
return appendRelativePath(directoryName, true)
|
||||
}
|
||||
|
||||
private data class QueryResult(
|
||||
val uri: Uri,
|
||||
val lastModified: Long,
|
||||
val length: Long,
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun query(displayName: String = namePath): QueryResult? {
|
||||
try {
|
||||
//val (name, mime) = splitFilenameMime(fullName)
|
||||
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
)
|
||||
|
||||
val selection =
|
||||
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'"
|
||||
|
||||
contentResolver.query(
|
||||
baseUri,
|
||||
projection, selection, null, null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val id =
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
||||
|
||||
return QueryResult(
|
||||
uri = ContentUris.withAppendedId(
|
||||
baseUri, id
|
||||
),
|
||||
lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)),
|
||||
length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun uri(): Uri? {
|
||||
return query()?.uri
|
||||
}
|
||||
|
||||
override fun name(): String? {
|
||||
if (isDir) return null
|
||||
return namePath
|
||||
}
|
||||
|
||||
override fun type(): String? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun filePath(): String {
|
||||
return replaceDuplicateFileSeparators(relativePath + File.separator + namePath)
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean {
|
||||
return isDir
|
||||
}
|
||||
|
||||
override fun isFile(): Boolean {
|
||||
return isFile
|
||||
}
|
||||
|
||||
override fun lastModified(): Long? {
|
||||
if (isDir) return null
|
||||
return query()?.lastModified
|
||||
}
|
||||
|
||||
override fun length(): Long? {
|
||||
if (isDir) return null
|
||||
val query = query()
|
||||
val length = query?.length ?: return null
|
||||
if (length <= 0) {
|
||||
try {
|
||||
contentResolver.openFileDescriptor(query.uri, "r")
|
||||
.use {
|
||||
it?.statSize
|
||||
}?.let {
|
||||
return it
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
return null
|
||||
}
|
||||
|
||||
val inputStream: InputStream = openInputStream() ?: return null
|
||||
return try {
|
||||
inputStream.available().toLong()
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
} finally {
|
||||
inputStream.closeQuietly()
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
override fun canRead(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun canWrite(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
private fun delete(uri: Uri): Boolean {
|
||||
return contentResolver.delete(uri, null, null) > 0
|
||||
}
|
||||
|
||||
override fun delete(): Boolean {
|
||||
return if (isDir) {
|
||||
(listFiles() ?: return false).all {
|
||||
it.delete()
|
||||
}
|
||||
} else {
|
||||
delete(uri() ?: return false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun exists(): Boolean {
|
||||
if (isDir) return true
|
||||
return query() != null
|
||||
}
|
||||
|
||||
override fun listFiles(): List<SafeFile>? {
|
||||
if (isFile) return null
|
||||
try {
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns.DISPLAY_NAME
|
||||
)
|
||||
|
||||
val selection =
|
||||
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'"
|
||||
contentResolver.query(
|
||||
baseUri,
|
||||
projection, selection, null, null
|
||||
)?.use { cursor ->
|
||||
val out = ArrayList<SafeFile>(cursor.count)
|
||||
while (cursor.moveToNext()) {
|
||||
val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||
if (nameIdx == -1) continue
|
||||
val name = cursor.getString(nameIdx)
|
||||
|
||||
appendRelativePath(name, false)?.let { new ->
|
||||
out.add(new)
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? {
|
||||
if (isFile || displayName == null) return null
|
||||
|
||||
val new = appendRelativePath(displayName, false) ?: return null
|
||||
if (new.exists()) {
|
||||
return new
|
||||
}
|
||||
|
||||
return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null)
|
||||
}
|
||||
|
||||
override fun renameTo(name: String?): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun openOutputStream(append: Boolean): OutputStream? {
|
||||
try {
|
||||
// use current file
|
||||
uri()?.let {
|
||||
return contentResolver.openOutputStream(
|
||||
it,
|
||||
if (append) "wa" else "wt"
|
||||
)
|
||||
}
|
||||
|
||||
// create a new file if current is not found,
|
||||
// as we know it is new only write access is needed
|
||||
createUri()?.let {
|
||||
return contentResolver.openOutputStream(
|
||||
it,
|
||||
"w"
|
||||
)
|
||||
}
|
||||
return null
|
||||
} catch (t: Throwable) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun openInputStream(): InputStream? {
|
||||
try {
|
||||
return contentResolver.openInputStream(uri() ?: return null)
|
||||
} catch (t: Throwable) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
package com.lagradost.cloudstream3.utils.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.UniRandomAccessFile
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
interface SafeFile {
|
||||
companion object {
|
||||
fun fromUri(context: Context, uri: Uri): SafeFile? {
|
||||
return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null)
|
||||
}
|
||||
|
||||
fun fromFile(context: Context, file: File?): SafeFile? {
|
||||
if (file == null) return null
|
||||
// because UniFile sucks balls on Media we have to do this
|
||||
val absPath = file.absolutePath.removePrefix(File.separator)
|
||||
for (value in MediaFileContentType.values()) {
|
||||
val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) }
|
||||
for (prefix in prefixes) {
|
||||
if (!absPath.startsWith(prefix)) continue
|
||||
return fromMedia(
|
||||
context,
|
||||
value,
|
||||
absPath.removePrefix(prefix).ifBlank { File.separator }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return UniFileWrapper(UniFile.fromFile(file) ?: return null)
|
||||
}
|
||||
|
||||
fun fromAsset(
|
||||
context: Context,
|
||||
filename: String?
|
||||
): SafeFile? {
|
||||
return UniFileWrapper(
|
||||
UniFile.fromAsset(context.assets, filename ?: return null) ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
fun fromResource(
|
||||
context: Context,
|
||||
id: Int
|
||||
): SafeFile? {
|
||||
return UniFileWrapper(
|
||||
UniFile.fromResource(context, id) ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
fun fromMedia(
|
||||
context: Context,
|
||||
folderType: MediaFileContentType,
|
||||
path: String = File.separator,
|
||||
external: Boolean = true,
|
||||
): SafeFile? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
//fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path)
|
||||
|
||||
return MediaFile(
|
||||
context = context,
|
||||
folderType = folderType,
|
||||
external = external,
|
||||
absolutePath = path
|
||||
)
|
||||
} else {
|
||||
fromFile(
|
||||
context,
|
||||
File(
|
||||
(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||
folderType.toPath() + File.separator + folderType).replace(
|
||||
File.separator + File.separator,
|
||||
File.separator
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*val uri: Uri? get() = getUri()
|
||||
val name: String? get() = getName()
|
||||
val type: String? get() = getType()
|
||||
val filePath: String? get() = getFilePath()
|
||||
val isFile: Boolean? get() = isFile()
|
||||
val isDirectory: Boolean? get() = isDirectory()
|
||||
val length: Long? get() = length()
|
||||
val canRead: Boolean get() = canRead()
|
||||
val canWrite: Boolean get() = canWrite()
|
||||
val lastModified: Long? get() = lastModified()*/
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun isFileOrThrow(): Boolean {
|
||||
return isFile() ?: throw IOException("Unable to get if file is a file or directory")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun lengthOrThrow(): Long {
|
||||
return length() ?: throw IOException("Unable to get file length")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun isDirectoryOrThrow(): Boolean {
|
||||
return isDirectory() ?: throw IOException("Unable to get if file is a directory")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun filePathOrThrow(): String {
|
||||
return filePath() ?: throw IOException("Unable to get file path")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun uriOrThrow(): Uri {
|
||||
return uri() ?: throw IOException("Unable to get uri")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun renameOrThrow(name: String?) {
|
||||
if (!renameTo(name)) {
|
||||
throw IOException("Unable to rename to $name")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun openOutputStreamOrThrow(append: Boolean = false): OutputStream {
|
||||
return openOutputStream(append) ?: throw IOException("Unable to open output stream")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun openInputStreamOrThrow(): InputStream {
|
||||
return openInputStream() ?: throw IOException("Unable to open input stream")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun existsOrThrow(): Boolean {
|
||||
return exists() ?: throw IOException("Unable get if file exists")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile {
|
||||
return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun gotoDirectoryOrThrow(
|
||||
directoryName: String?,
|
||||
createMissingDirectories: Boolean = true
|
||||
): SafeFile {
|
||||
return gotoDirectory(directoryName, createMissingDirectories)
|
||||
?: throw IOException("Unable to go to directory $directoryName")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun listFilesOrThrow(): List<SafeFile> {
|
||||
return listFiles() ?: throw IOException("Unable to get files")
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun createFileOrThrow(displayName: String?): SafeFile {
|
||||
return createFile(displayName) ?: throw IOException("Unable to create file $displayName")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun createDirectoryOrThrow(directoryName: String?): SafeFile {
|
||||
return createDirectory(
|
||||
directoryName ?: throw IOException("Unable to create file with invalid name")
|
||||
)
|
||||
?: throw IOException("Unable to create directory $directoryName")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun deleteOrThrow() {
|
||||
if (!delete()) {
|
||||
throw IOException("Unable to delete file")
|
||||
}
|
||||
}
|
||||
|
||||
/** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName
|
||||
* returns itself. createMissingDirectories specifies if the dirs should be created
|
||||
* when travelling or break at a dir not found */
|
||||
fun gotoDirectory(
|
||||
directoryName: String?,
|
||||
createMissingDirectories: Boolean = true
|
||||
): SafeFile? {
|
||||
if (directoryName == null) return this
|
||||
|
||||
return directoryName.split(File.separatorChar).filter { it.isNotBlank() }
|
||||
.fold(this) { file: SafeFile?, directory ->
|
||||
// as MediaFile does not actually create a directory we can do this
|
||||
if (createMissingDirectories || this is MediaFile) {
|
||||
file?.createDirectory(directory)
|
||||
} else {
|
||||
val next = file?.findFile(directory)
|
||||
|
||||
// we require the file to be a directory
|
||||
if (next?.isDirectory() != true) {
|
||||
null
|
||||
} else {
|
||||
next
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun createFile(displayName: String?): SafeFile?
|
||||
fun createDirectory(directoryName: String?): SafeFile?
|
||||
fun uri(): Uri?
|
||||
fun name(): String?
|
||||
fun type(): String?
|
||||
fun filePath(): String?
|
||||
fun isDirectory(): Boolean?
|
||||
fun isFile(): Boolean?
|
||||
fun lastModified(): Long?
|
||||
fun length(): Long?
|
||||
fun canRead(): Boolean
|
||||
fun canWrite(): Boolean
|
||||
fun delete(): Boolean
|
||||
fun exists(): Boolean?
|
||||
fun listFiles(): List<SafeFile>?
|
||||
|
||||
// fun listFiles(filter: FilenameFilter?): Array<File>?
|
||||
fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile?
|
||||
|
||||
fun renameTo(name: String?): Boolean
|
||||
|
||||
/** Open a stream on to the content associated with the file */
|
||||
fun openOutputStream(append: Boolean = false): OutputStream?
|
||||
|
||||
/** Open a stream on to the content associated with the file */
|
||||
fun openInputStream(): InputStream?
|
||||
|
||||
/** Get a random access stuff of the UniFile, "r" or "rw" */
|
||||
fun createRandomAccessFile(mode: String?): UniRandomAccessFile?
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package com.lagradost.cloudstream3.utils.storage
|
||||
|
||||
import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.UniRandomAccessFile
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import okhttp3.internal.closeQuietly
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
private fun UniFile.toFile(): SafeFile {
|
||||
return UniFileWrapper(this)
|
||||
}
|
||||
|
||||
fun <T> safe(apiCall: () -> T): T? {
|
||||
return try {
|
||||
apiCall.invoke()
|
||||
} catch (throwable: Throwable) {
|
||||
logError(throwable)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
class UniFileWrapper(val file: UniFile) : SafeFile {
|
||||
override fun createFile(displayName: String?): SafeFile? {
|
||||
return file.createFile(displayName)?.toFile()
|
||||
}
|
||||
|
||||
override fun createDirectory(directoryName: String?): SafeFile? {
|
||||
return file.createDirectory(directoryName)?.toFile()
|
||||
}
|
||||
|
||||
override fun uri(): Uri? {
|
||||
return safe { file.uri }
|
||||
}
|
||||
|
||||
override fun name(): String? {
|
||||
return safe { file.name }
|
||||
}
|
||||
|
||||
override fun type(): String? {
|
||||
return safe { file.type }
|
||||
}
|
||||
|
||||
override fun filePath(): String? {
|
||||
return safe { file.filePath }
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean? {
|
||||
return safe { file.isDirectory }
|
||||
}
|
||||
|
||||
override fun isFile(): Boolean? {
|
||||
return safe { file.isFile }
|
||||
}
|
||||
|
||||
override fun lastModified(): Long? {
|
||||
return safe { file.lastModified() }
|
||||
}
|
||||
|
||||
override fun length(): Long? {
|
||||
return safe {
|
||||
val len = file.length()
|
||||
if (len <= 1) {
|
||||
val inputStream = this.openInputStream() ?: return@safe null
|
||||
try {
|
||||
inputStream.available().toLong()
|
||||
} finally {
|
||||
inputStream.closeQuietly()
|
||||
}
|
||||
} else {
|
||||
len
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun canRead(): Boolean {
|
||||
return safe { file.canRead() } ?: false
|
||||
}
|
||||
|
||||
override fun canWrite(): Boolean {
|
||||
return safe { file.canWrite() } ?: false
|
||||
}
|
||||
|
||||
override fun delete(): Boolean {
|
||||
return safe { file.delete() } ?: false
|
||||
}
|
||||
|
||||
override fun exists(): Boolean? {
|
||||
return safe { file.exists() }
|
||||
}
|
||||
|
||||
override fun listFiles(): List<SafeFile>? {
|
||||
return safe { file.listFiles()?.mapNotNull { it?.toFile() } }
|
||||
}
|
||||
|
||||
override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? {
|
||||
return safe { file.findFile(displayName, ignoreCase)?.toFile() }
|
||||
}
|
||||
|
||||
override fun renameTo(name: String?): Boolean {
|
||||
return safe { file.renameTo(name) } ?: return false
|
||||
}
|
||||
|
||||
override fun openOutputStream(append: Boolean): OutputStream? {
|
||||
return safe { file.openOutputStream(append) }
|
||||
}
|
||||
|
||||
override fun openInputStream(): InputStream? {
|
||||
return safe { file.openInputStream() }
|
||||
}
|
||||
|
||||
override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? {
|
||||
return safe { file.createRandomAccessFile(mode) }
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue