changes to downloader for stable resume

This commit is contained in:
LagradOst 2023-08-22 04:00:05 +02:00
parent 4e28e5f8cc
commit afcbdeecc8
4 changed files with 191 additions and 380 deletions

View file

@ -520,10 +520,10 @@ class GeneratorPlayer : FullScreenPlayer() {
if (uri == null) return@normalSafeApiCall if (uri == null) return@normalSafeApiCall
val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall
// RW perms for the path // RW perms for the path
val flags = ctx.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
ctx.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(ctx, uri) val file = UniFile.fromUri(ctx, uri)
println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}") println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}")

View file

@ -116,13 +116,14 @@ class SettingsUpdates : PreferenceFragmentCompat() {
null, null,
"txt", "txt",
false false
).fileStream ).openNew()
fileStream?.writer()?.write(text) fileStream.writer().write(text)
} catch (e: Exception) { dialog.dismissSafe(activity)
logError(e) } catch (t: Throwable) {
logError(t)
showToast(t.message)
} finally { } finally {
fileStream?.closeQuietly() fileStream?.closeQuietly()
dialog.dismissSafe(activity)
} }
} }
binding.closeBtt.setOnClickListener { binding.closeBtt.setOnClickListener {

View file

@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadCheck
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadEpisode import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadEpisode
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadFromResume import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadFromResume
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadStatusEvent import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadStatusEvent
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadResumePackage
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
const val DOWNLOAD_CHECK = "DownloadCheck" const val DOWNLOAD_CHECK = "DownloadCheck"
@ -36,15 +37,20 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo
WORK_KEY_PACKAGE, WORK_KEY_PACKAGE,
key key
) )
if (info != null) { if (info != null) {
downloadEpisode( getDownloadResumePackage(applicationContext, info.ep.id)?.let { dpkg ->
applicationContext, downloadFromResume(applicationContext, dpkg, ::handleNotification)
info.source, } ?: run {
info.folder, downloadEpisode(
info.ep, applicationContext,
info.links, info.source,
::handleNotification info.folder,
) info.ep,
info.links,
::handleNotification
)
}
} else if (pkg != null) { } else if (pkg != null) {
downloadFromResume(applicationContext, pkg, ::handleNotification) downloadFromResume(applicationContext, pkg, ::handleNotification)
} }

View file

@ -9,9 +9,7 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
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
@ -32,6 +30,7 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.services.VideoDownloadService
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -44,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -301,6 +301,8 @@ object VideoDownloadManager {
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false) builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false)
} else if (state == DownloadType.IsPending) {
builder.setProgress(0,0,true)
} }
val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else "" val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else ""
@ -352,6 +354,10 @@ object VideoDownloadManager {
(if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString" (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString"
} }
DownloadType.IsPending -> {
(if (linkName == null) "" else "$linkName\n") + rowTwo
}
DownloadType.IsFailed -> { DownloadType.IsFailed -> {
downloadFormat.format( downloadFormat.format(
context.getString(R.string.download_failed), context.getString(R.string.download_failed),
@ -363,7 +369,7 @@ object VideoDownloadManager {
downloadFormat.format(context.getString(R.string.download_done), rowTwo) downloadFormat.format(context.getString(R.string.download_done), rowTwo)
} }
else -> { DownloadType.IsStopped -> {
downloadFormat.format( downloadFormat.format(
context.getString(R.string.download_canceled), context.getString(R.string.download_canceled),
rowTwo rowTwo
@ -377,7 +383,7 @@ object VideoDownloadManager {
} else { } else {
val txt = val txt =
when (state) { when (state) {
DownloadType.IsDownloading, DownloadType.IsPaused -> { DownloadType.IsDownloading, DownloadType.IsPaused, DownloadType.IsPending -> {
rowTwo rowTwo
} }
@ -392,7 +398,7 @@ object VideoDownloadManager {
downloadFormat.format(context.getString(R.string.download_done), rowTwo) downloadFormat.format(context.getString(R.string.download_done), rowTwo)
} }
else -> { DownloadType.IsStopped -> {
downloadFormat.format( downloadFormat.format(
context.getString(R.string.download_canceled), context.getString(R.string.download_canceled),
rowTwo rowTwo
@ -480,54 +486,6 @@ object VideoDownloadManager {
return tempName.replace(" ", " ").trim(' ') return tempName.replace(" ", " ").trim(' ')
} }
@RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingFolderStartName(relativePath: String): List<Pair<String, Uri>>? {
try {
val projection = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only)
//MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only)
)
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath'"
val result = this.query(
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
projection, selection, null, null
)
val list = ArrayList<Pair<String, Uri>>()
result.use { c ->
if (c != null && c.count >= 1) {
c.moveToFirst()
while (true) {
val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val name =
c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
val uri = ContentUris.withAppendedId(
MediaStore.Downloads.EXTERNAL_CONTENT_URI, id
)
list.add(Pair(name, uri))
if (c.isLast) {
break
}
c.moveToNext()
}
/*
val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/
}
}
return list
} catch (e: Exception) {
logError(e)
return null
}
}
/** /**
* Used for getting video player subs. * Used for getting video player subs.
* @return List of pairs for the files in this format: <Name, Uri> * @return List of pairs for the files in this format: <Name, Uri>
@ -538,76 +496,12 @@ object VideoDownloadManager {
basePath: String? basePath: String?
): List<Pair<String, Uri>>? { ): List<Pair<String, Uri>>? {
val base = basePathToFile(context, basePath) val base = basePathToFile(context, basePath)
val folder = base?.gotoDir(relativePath, false) val folder = base?.gotoDir(relativePath, false) ?: return null
if (!folder.isDirectory) return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { return folder.listFiles()?.map { (it.name ?: "") to it.uri }
return context.contentResolver?.getExistingFolderStartName(relativePath)
} else {
// 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)
private fun ContentResolver.getExistingDownloadUriOrNullQ(
relativePath: String,
displayName: String
): Uri? {
try {
val projection = arrayOf(
MediaStore.MediaColumns._ID,
//MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only)
//MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only)
)
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath' AND " + "${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'"
val result = this.query(
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
projection, selection, null, null
)
result.use { c ->
if (c != null && c.count >= 1) {
c.moveToFirst().let {
val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
/*
val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/
return ContentUris.withAppendedId(
MediaStore.Downloads.EXTERNAL_CONTENT_URI, id
)
}
}
}
return null
} catch (e: Exception) {
logError(e)
return null
}
}
@RequiresApi(Build.VERSION_CODES.Q)
fun ContentResolver.getFileLength(fileUri: Uri): Long? {
return try {
this.openFileDescriptor(fileUri, "r")
.use { it?.statSize ?: 0 }
} catch (e: Exception) {
logError(e)
null
}
}
data class CreateNotificationMetadata( data class CreateNotificationMetadata(
val type: DownloadType, val type: DownloadType,
@ -619,16 +513,39 @@ object VideoDownloadManager {
) )
data class StreamData( data class StreamData(
val errorCode: Int, private val fileLength: Long,
val resume: Boolean? = null, val file: UniFile,
val fileLength: Long? = null, //val fileStream: OutputStream,
val fileStream: OutputStream? = null, ) {
) fun open() : OutputStream {
return file.openOutputStream(resume)
}
fun openNew() : OutputStream {
return file.openOutputStream(false)
}
val resume: Boolean get() = fileLength > 0L
val startAt: Long get() = if (resume) fileLength else 0L
val exists: Boolean get() = file.exists()
}
//class ADownloadException(val id: Int) : RuntimeException(message = "Download error $id")
fun UniFile.createFileOrThrow(displayName: String): UniFile {
return this.createFile(displayName) ?: throw IOException("Could not create file")
}
fun UniFile.deleteOrThrow() {
if (!this.delete()) throw IOException("Could not delete file")
}
/** /**
* Sets up the appropriate file and creates a data stream from the file. * Sets up the appropriate file and creates a data stream from the file.
* Used for initializing downloads. * Used for initializing downloads.
* */ * */
@Throws(IOException::class)
fun setupStream( fun setupStream(
context: Context, context: Context,
name: String, name: String,
@ -637,88 +554,24 @@ object VideoDownloadManager {
tryResume: Boolean, tryResume: Boolean,
): StreamData { ): StreamData {
val displayName = getDisplayName(name, extension) val displayName = getDisplayName(name, extension)
val fileStream: OutputStream
val fileLength: Long
var resume = tryResume
val baseFile = context.getBasePath()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.first?.isDownloadDir() == true) { val (baseFile, _) = context.getBasePath()
val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
val currentExistingFile = val subDir = baseFile?.gotoDir(folder) ?: throw IOException()
cr.getExistingDownloadUriOrNullQ( val foundFile = subDir.findFile(displayName)
folder ?: "",
displayName
) // CURRENT FILE WITH THE SAME PATH
fileLength = val (file, fileLength) = if (foundFile == null || !foundFile.exists()) {
if (currentExistingFile == null || !resume) 0 else (cr.getFileLength( subDir.createFileOrThrow(displayName) to 0L
currentExistingFile
)
?: 0)// IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE
if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME
val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null)
if (rowsDeleted < 1) {
println("ERROR DELETING FILE!!!")
}
}
var appendFile = false
val newFileUri = if (resume && currentExistingFile != null) {
appendFile = true
currentExistingFile
} else {
val contentUri =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val currentMimeType = when (extension) {
// 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
}
val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, name)
if (currentMimeType != null)
put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
}
cr.insert(
contentUri,
newFile
) ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
}
fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else ""))
?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
} else { } else {
val subDir = baseFile.first?.gotoDir(folder) if (tryResume) {
val rFile = subDir?.findFile(displayName) foundFile to foundFile.size()
if (rFile?.exists() != true) {
fileLength = 0
if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE)
} else { } else {
if (resume) { foundFile.deleteOrThrow()
fileLength = rFile.size() subDir.createFileOrThrow(displayName) to 0L
} else {
fileLength = 0
if (!rFile.delete()) return StreamData(ERROR_DELETING_FILE)
if (subDir.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE)
}
} }
fileStream = (subDir.findFile(displayName)
?: subDir.createFile(displayName))!!.openOutputStream()
// fileStream = FileOutputStream(rFile, false)
if (fileLength == 0L) resume = false
} }
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
return StreamData(fileLength, file)
} }
/** This class handles the notifications, as well as the relevant key */ /** This class handles the notifications, as well as the relevant key */
@ -938,6 +791,8 @@ object VideoDownloadManager {
fun setWrittenSegment(segmentIndex: Int) { fun setWrittenSegment(segmentIndex: Int) {
hlsWrittenProgress = segmentIndex + 1 hlsWrittenProgress = segmentIndex + 1
// in case of abort we need to save every written progress
updateFileInfo()
} }
} }
@ -1185,18 +1040,16 @@ object VideoDownloadManager {
// set up the download file // set up the download file
val stream = setupStream(context, name, relativePath, extension, tryResume) val stream = setupStream(context, name, relativePath, extension, tryResume)
if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode
fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN fileStream = stream.open()
val resume = stream.resume ?: return@withContext ERROR_UNKNOWN
val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN metadata.setResumeLength(stream.startAt)
val resumeAt = (if (resume) fileLength else 0)
metadata.setResumeLength(resumeAt)
metadata.type = DownloadType.IsPending metadata.type = DownloadType.IsPending
val items = streamLazy( val items = streamLazy(
url = link.url.replace(" ", "%20"), url = link.url.replace(" ", "%20"),
referer = link.referer, referer = link.referer,
startByte = resumeAt, startByte = stream.startAt,
headers = link.headers.appendAndDontOverride( headers = link.headers.appendAndDontOverride(
mapOf( mapOf(
"Accept-Encoding" to "identity", "Accept-Encoding" to "identity",
@ -1230,6 +1083,19 @@ object VideoDownloadManager {
val pendingData: HashMap<Long, LazyStreamDownloadResponse> = val pendingData: HashMap<Long, LazyStreamDownloadResponse> =
hashMapOf() hashMapOf()
val fileChecker = launch(Dispatchers.IO) {
while (isActive) {
if (stream.exists) {
delay(5000)
continue
}
fileMutex.withLock {
metadata.type = DownloadType.IsStopped
}
break
}
}
val jobs = (0 until parallelConnections).map { val jobs = (0 until parallelConnections).map {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
@ -1329,9 +1195,11 @@ object VideoDownloadManager {
} }
jobs.join() jobs.join()
fileChecker.cancel()
// jobs are finished so we don't want to stop them anymore // jobs are finished so we don't want to stop them anymore
metadata.removeStopListener() metadata.removeStopListener()
if (!stream.exists) metadata.type = DownloadType.IsStopped
if (metadata.type == DownloadType.IsFailed) { if (metadata.type == DownloadType.IsFailed) {
return@withContext ERROR_CONNECTION_ERROR return@withContext ERROR_CONNECTION_ERROR
@ -1341,11 +1209,8 @@ object VideoDownloadManager {
// we need to close before delete // we need to close before delete
fileStream.closeQuietly() fileStream.closeQuietly()
metadata.onDelete() metadata.onDelete()
if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { deleteFile(context, baseFile, relativePath ?: "", displayName)
return@withContext SUCCESS_STOPPED return@withContext SUCCESS_STOPPED
} else {
return@withContext ERROR_DELETING_FILE
}
} }
metadata.type = DownloadType.IsDone metadata.type = DownloadType.IsDone
@ -1400,13 +1265,13 @@ object VideoDownloadManager {
folder folder
) else folder ) else folder
val displayName = getDisplayName(name, extension) val displayName = getDisplayName(name, extension)
val stream = setupStream(context, name, relativePath, extension, startAt > 0) val stream =
if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode setupStream(context, name, relativePath, extension, startAt > 0)
if (stream.resume != true) startAt = 0 if (!stream.resume) startAt = 0
fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN fileStream = stream.open()
// push the metadata // push the metadata
metadata.setResumeLength(stream.fileLength ?: 0) metadata.setResumeLength(stream.startAt)
metadata.hlsProgress = startAt metadata.hlsProgress = startAt
metadata.type = DownloadType.IsPending metadata.type = DownloadType.IsPending
metadata.setDownloadFileInfoTemplate( metadata.setDownloadFileInfoTemplate(
@ -1433,13 +1298,25 @@ object VideoDownloadManager {
metadata.hlsTotal = items.size metadata.hlsTotal = items.size
metadata.type = DownloadType.IsDownloading metadata.type = DownloadType.IsDownloading
val currentMutex = Mutex() val currentMutex = Mutex()
val current = (0 until items.size).iterator() val current = (startAt until items.size).iterator()
val fileMutex = Mutex() val fileMutex = Mutex()
val pendingData: HashMap<Int, ByteArray> = hashMapOf() val pendingData: HashMap<Int, ByteArray> = hashMapOf()
val fileChecker = launch(Dispatchers.IO) {
while (isActive) {
if (stream.exists) {
delay(5000)
continue
}
fileMutex.withLock {
metadata.type = DownloadType.IsStopped
}
break
}
}
// see @downloadexplanation for explanation of this download strategy, // see @downloadexplanation for explanation of this download strategy,
// this keeps all jobs working at all times, // this keeps all jobs working at all times,
// does several connections in parallel instead of a regular for loop to improve // does several connections in parallel instead of a regular for loop to improve
@ -1476,7 +1353,7 @@ object VideoDownloadManager {
// user pause // user pause
while (metadata.type == DownloadType.IsPaused) delay(100) while (metadata.type == DownloadType.IsPaused) delay(100)
// if stopped then break to delete // if stopped then break to delete
if (metadata.type == DownloadType.IsStopped || !isActive) return@launch if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed || !isActive) return@launch
val segmentLength = bytes.size.toLong() val segmentLength = bytes.size.toLong()
// send notification, no matter the actual write order // send notification, no matter the actual write order
@ -1499,11 +1376,13 @@ object VideoDownloadManager {
val cacheLength = cache.size.toLong() val cacheLength = cache.size.toLong()
fileStream.write(cache) fileStream.write(cache)
metadata.addBytesWritten(cacheLength) metadata.addBytesWritten(cacheLength)
metadata.setWrittenSegment(metadata.hlsWrittenProgress) metadata.setWrittenSegment(metadata.hlsWrittenProgress)
} }
} catch (t: Throwable) { } catch (t: Throwable) {
// this is in case of write fail // this is in case of write fail
logError(t)
if (metadata.type != DownloadType.IsStopped) { if (metadata.type != DownloadType.IsStopped) {
metadata.type = DownloadType.IsFailed metadata.type = DownloadType.IsFailed
} }
@ -1520,9 +1399,12 @@ object VideoDownloadManager {
} }
jobs.join() jobs.join()
fileChecker.cancel()
metadata.removeStopListener() metadata.removeStopListener()
if (!stream.exists) metadata.type = DownloadType.IsStopped
if (metadata.type == DownloadType.IsFailed) { if (metadata.type == DownloadType.IsFailed) {
return@withContext ERROR_CONNECTION_ERROR return@withContext ERROR_CONNECTION_ERROR
} }
@ -1531,11 +1413,8 @@ object VideoDownloadManager {
// we need to close before delete // we need to close before delete
fileStream.closeQuietly() fileStream.closeQuietly()
metadata.onDelete() metadata.onDelete()
if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { deleteFile(context, baseFile, relativePath ?: "", displayName)
return@withContext SUCCESS_STOPPED return@withContext SUCCESS_STOPPED
} else {
return@withContext ERROR_DELETING_FILE
}
} }
metadata.type = DownloadType.IsDone metadata.type = DownloadType.IsDone
@ -1564,6 +1443,11 @@ object VideoDownloadManager {
directoryName: String?, directoryName: String?,
createMissingDirectories: Boolean = true createMissingDirectories: Boolean = true
): UniFile? { ): UniFile? {
if(directoryName == null) return this
return directoryName.split(File.separatorChar).filter { it.isNotBlank() }.fold(this) { file: UniFile?, directory ->
file?.createDirectory(directory)
}
// May give this error on scoped storage. // May give this error on scoped storage.
// W/DocumentsContract: Failed to create document // W/DocumentsContract: Failed to create document
@ -1571,7 +1455,7 @@ object VideoDownloadManager {
// Not present in latest testing. // Not present in latest testing.
// println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}")
try { try {
// Creates itself from parent if doesn't exist. // Creates itself from parent if doesn't exist.
@ -1671,49 +1555,6 @@ object VideoDownloadManager {
return this != null && this.filePath == getDownloadDir()?.filePath return this != null && this.filePath == getDownloadDir()?.filePath
} }
/*private fun delete(
context: Context,
name: String,
folder: String?,
extension: String,
parentId: Int?,
basePath: UniFile?
): Int {
val displayName = getDisplayName(name, extension)
// delete all subtitle files
if (extension != "vtt" && extension != "srt") {
try {
delete(context, name, folder, "vtt", parentId, basePath)
delete(context, name, folder, "srt", parentId, basePath)
} catch (e: Exception) {
logError(e)
}
}
// If scoped storage and using download dir (not accessible with UniFile)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) {
val relativePath = getRelativePath(folder)
val lastContent =
context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return ERROR_DELETING_FILE
if(context.contentResolver.delete(lastContent, null, null) <= 0) {
return ERROR_DELETING_FILE
}
} else {
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 {
downloadDeleteEvent.invoke(parentId)
}
}
return SUCCESS_STOPPED
}*/
fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String {
return getFileName(context, metadata.name, metadata.episode, metadata.season) return getFileName(context, metadata.name, metadata.episode, metadata.season)
@ -1765,70 +1606,60 @@ object VideoDownloadManager {
} }
} }
if (link.isM3u8 || URL(link.url).path.endsWith(".m3u8")) { val callback: (CreateNotificationMetadata) -> Unit = { meta ->
val startIndex = if (tryResume) { main {
context.getKey<DownloadedFileInfo>( createNotification(
KEY_DOWNLOAD_INFO, context,
ep.id.toString(), source,
null link.name,
)?.extraInfo?.toIntOrNull() ep,
} else null meta.type,
return suspendSafeApiCall { meta.bytesDownloaded,
downloadHLS( meta.bytesTotal,
notificationCallback,
meta.hlsProgress,
meta.hlsTotal,
meta.bytesPerSecond
)
}
}
try {
if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) {
val startIndex = if (tryResume) {
context.getKey<DownloadedFileInfo>(
KEY_DOWNLOAD_INFO,
ep.id.toString(),
null
)?.extraInfo?.toIntOrNull()
} else null
return downloadHLS(
context, context,
link, link,
name, name,
folder, folder,
ep.id, ep.id,
startIndex, startIndex,
createNotificationCallback = { meta -> callback
main {
createNotification(
context,
source,
link.name,
ep,
meta.type,
meta.bytesDownloaded,
meta.bytesTotal,
notificationCallback,
meta.hlsProgress,
meta.hlsTotal,
meta.bytesPerSecond
)
}
}
) )
}.also { } else {
extractorJob.cancel() return downloadThing(
} ?: ERROR_UNKNOWN context,
link,
name,
folder,
"mp4",
tryResume,
ep.id,
callback
)
}
} catch (t: Throwable) {
return ERROR_UNKNOWN
} finally {
extractorJob.cancel()
} }
return suspendSafeApiCall {
downloadThing(
context,
link,
name,
folder,
"mp4",
tryResume,
ep.id,
createNotificationCallback = { meta ->
main {
createNotification(
context,
source,
link.name,
ep,
meta.type,
meta.bytesDownloaded,
meta.bytesTotal,
notificationCallback,
bytesPerSecond = meta.bytesPerSecond
)
}
})
}.also { extractorJob.cancel() } ?: ERROR_UNKNOWN
} }
suspend fun downloadCheck( suspend fun downloadCheck(
@ -1911,26 +1742,10 @@ object VideoDownloadManager {
val info = val info =
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null
val base = basePathToFile(context, info.basePath) val base = basePathToFile(context, info.basePath)
val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName)
if (file?.exists() != true) return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri)
val cr = context.contentResolver ?: return null
val fileUri =
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName)
?: return null
val fileLength = cr.getFileLength(fileUri) ?: return null
if (fileLength == 0L) return null
return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri)
} else {
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.size(), info.totalBytes, file.uri)
}
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
return null return null
@ -1943,6 +1758,7 @@ object VideoDownloadManager {
fun UniFile.size(): Long { fun UniFile.size(): Long {
val len = length() val len = length()
return if (len <= 1) { return if (len <= 1) {
println("LEN:::::::>>>>>>>>>>>>>>>>>>>>>>>$len")
val inputStream = this.openInputStream() val inputStream = this.openInputStream()
return inputStream.available().toLong().also { inputStream.closeQuietly() } return inputStream.available().toLong().also { inputStream.closeQuietly() }
} else { } else {
@ -1962,32 +1778,20 @@ object VideoDownloadManager {
relativePath: String, relativePath: String,
displayName: String displayName: String
): Boolean { ): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && folder.isDownloadDir()) { val file = folder?.gotoDir(relativePath)?.findFile(displayName) ?: return false
val cr = context.contentResolver ?: return false if (!file.exists()) return true
val fileUri = return try {
cr.getExistingDownloadUriOrNullQ(relativePath, displayName) file.delete()
?: return true // FILE NOT FOUND, ALREADY DELETED } catch (e: Exception) {
logError(e)
return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 (context.contentResolver?.delete(file.uri, null, null) ?: return false) > 0
} else {
val file = folder?.gotoDir(relativePath)?.findFile(displayName)
// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
// val dFile = File(normalPath)
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
}
} }
} }
private fun deleteFile(context: Context, id: Int): Boolean { private fun deleteFile(context: Context, id: Int): Boolean {
val info = val info =
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return false context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return false
downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) downloadEvent.invoke(id to DownloadActionType.Stop)
downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadProgressEvent.invoke(Triple(id, 0, 0))
downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadStatusEvent.invoke(id to DownloadType.IsStopped)
downloadDeleteEvent.invoke(id) downloadDeleteEvent.invoke(id)