download system done?

This commit is contained in:
LagradOst 2021-07-05 22:28:50 +02:00
parent 18a727abe5
commit 5536f41ecb
2 changed files with 101 additions and 22 deletions

View file

@ -16,7 +16,7 @@ class VideoDownloadService : IntentService("VideoDownloadService") {
"stop" -> VideoDownloadManager.DownloadActionType.Stop "stop" -> VideoDownloadManager.DownloadActionType.Stop
else -> return else -> return
} }
VideoDownloadManager.events.invoke(Pair(id, state)) VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
} }
} }
} }

View file

@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.services.VideoDownloadService
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -41,7 +42,7 @@ const val CHANNEL_DESCRIPT = "The download notification channel"
object VideoDownloadManager { object VideoDownloadManager {
var maxConcurrentDownloads = 3 var maxConcurrentDownloads = 3
private var currentDownloads: Int = 0 private var currentDownloads = mutableListOf<Int>()
private const val USER_AGENT = private const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
@ -106,6 +107,17 @@ object VideoDownloadManager {
val linkIndex: Int?, val linkIndex: Int?,
) )
data class DownloadedFileInfo(
val totalBytes: Long,
val relativePath: String,
val displayName: String,
)
data class DownloadedFileInfoResult(
val totalBytes: Long,
val path: Uri,
)
private const val SUCCESS_DOWNLOAD_DONE = 1 private const val SUCCESS_DOWNLOAD_DONE = 1
private const val SUCCESS_STOPPED = 2 private const val SUCCESS_STOPPED = 2
private const val ERROR_DELETING_FILE = -1 private const val ERROR_DELETING_FILE = -1
@ -118,9 +130,11 @@ object VideoDownloadManager {
private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8 private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8
private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9 private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9
private const val KEY_RESUME_STORAGE = "download_resume" private const val KEY_RESUME_PACKAGES = "download_resume"
private const val KEY_DOWNLOAD_INFO = "download_info"
val events = Event<Pair<Int, DownloadActionType>>() val downloadEvent = Event<Pair<Int, DownloadActionType>>()
val downloadProgressEvent = Event<Pair<Int, Long>>()
private val downloadQueue = LinkedList<DownloadResumePackage>() private val downloadQueue = LinkedList<DownloadResumePackage>()
private var hasCreatedNotChanel = false private var hasCreatedNotChanel = false
@ -281,7 +295,7 @@ object VideoDownloadManager {
val pending: PendingIntent = PendingIntent.getService( val pending: PendingIntent = PendingIntent.getService(
// BECAUSE episodes lying near will have the same id +1, index will give the same requested as the previous episode, *100000 fixes this // BECAUSE episodes lying near will have the same id +1, index will give the same requested as the previous episode, *100000 fixes this
context, (4337 + index*100000 + ep.id), context, (4337 + index * 100000 + ep.id),
actionResultIntent, actionResultIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
@ -322,15 +336,6 @@ object VideoDownloadManager {
return tempName.replace(" ", " ").trim(' ') return tempName.replace(" ", " ").trim(' ')
} }
private const val reservedCharsPath = "|\\?*<\":>+[]\'"
fun sanitizePath(name: String): String {
var tempName = name
for (c in reservedCharsPath) {
tempName = tempName.replace(c, ' ')
}
return tempName.replace(" ", " ").trim(' ')
}
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? { private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
val projection = arrayOf( val projection = arrayOf(
@ -364,6 +369,12 @@ object VideoDownloadManager {
return null return null
} }
@RequiresApi(Build.VERSION_CODES.Q)
fun ContentResolver.getFileLength(fileUri: Uri): Long {
return this.openFileDescriptor(fileUri, "r")
.use { it?.statSize ?: 0 }
}
private fun isScopedStorage(): Boolean { private fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
} }
@ -406,8 +417,7 @@ object VideoDownloadManager {
cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
fileLength = fileLength =
if (currentExistingFile == null || !resume) 0 else cr.openFileDescriptor(currentExistingFile, "r") if (currentExistingFile == null || !resume) 0 else cr.getFileLength(currentExistingFile) // IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE
.use { it?.statSize ?: 0 } // IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE
if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME
val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null) val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null)
@ -476,6 +486,8 @@ object VideoDownloadManager {
val bytesTotal = contentLength + resumeLength val bytesTotal = contentLength + resumeLength
if (bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG if (bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
context.setKey(KEY_DOWNLOAD_INFO, ep.id.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName))
// Could use connection.contentType for mime types when creating the file, // Could use connection.contentType for mime types when creating the file,
// however file is already created and players don't go of file type // however file is already created and players don't go of file type
@ -517,7 +529,7 @@ object VideoDownloadManager {
) )
} }
events += { event -> downloadEvent += { event ->
if (event.first == ep.id) { if (event.first == ep.id) {
when (event.second) { when (event.second) {
DownloadActionType.Pause -> { DownloadActionType.Pause -> {
@ -545,12 +557,14 @@ object VideoDownloadManager {
} }
} }
val id = ep.id
// THE REAL READ // THE REAL READ
try { try {
while (true) { while (true) {
count = connectionInputStream.read(buffer) count = connectionInputStream.read(buffer)
if (count < 0) break if (count < 0) break
bytesDownloaded += count bytesDownloaded += count
downloadProgressEvent.invoke(Pair(id, bytesDownloaded))
while (isPaused) { while (isPaused) {
sleep(100) sleep(100)
if (isStopped) { if (isStopped) {
@ -589,10 +603,15 @@ object VideoDownloadManager {
} }
private fun downloadCheck(context: Context) { private fun downloadCheck(context: Context) {
if (currentDownloads < maxConcurrentDownloads && downloadQueue.size > 0) { if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
currentDownloads++
val pkg = downloadQueue.removeFirst() val pkg = downloadQueue.removeFirst()
val item = pkg.item val item = pkg.item
val id = item.ep.id
if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT
downloadEvent.invoke(Pair(id, DownloadActionType.Resume))
return
}
currentDownloads.add(id)
main { main {
try { try {
@ -600,27 +619,87 @@ object VideoDownloadManager {
val link = item.links[index] val link = item.links[index]
val resume = pkg.linkIndex == index val resume = pkg.linkIndex == index
context.setKey(KEY_RESUME_STORAGE, item.ep.id.toString(), DownloadResumePackage(item, index)) context.setKey(KEY_RESUME_PACKAGES, id.toString(), DownloadResumePackage(item, index))
val connectionResult = withContext(Dispatchers.IO) { val connectionResult = withContext(Dispatchers.IO) {
normalSafeApiCall { normalSafeApiCall {
downloadSingleEpisode(context, item.source, item.folder, item.ep, link, resume) downloadSingleEpisode(context, item.source, item.folder, item.ep, link, resume)
} }
} }
if (connectionResult != null && connectionResult > 0) { // SUCCESS if (connectionResult != null && connectionResult > 0) { // SUCCESS
context.removeKey(KEY_RESUME_STORAGE, item.ep.id.toString()) context.removeKey(KEY_RESUME_PACKAGES, id.toString())
break break
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} finally { } finally {
currentDownloads-- currentDownloads.remove(id)
downloadCheck(context) downloadCheck(context)
} }
} }
} }
} }
fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? {
val res = getDownloadFileInfo(context, id)
if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString())
return res
}
private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? {
val info = context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null
if (isScopedStorage()) {
val cr = context.contentResolver ?: return null
val fileUri =
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) ?: return null
val fileLength = cr.getFileLength(fileUri)
if (fileLength == 0L) return null
return DownloadedFileInfoResult(fileLength, fileUri)
} else {
val normalPath =
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${info.relativePath}${info.displayName}".replace(
'/',
File.separatorChar
)
val dFile = File(normalPath)
if (!dFile.exists()) return null
return DownloadedFileInfoResult(dFile.length(), dFile.toUri())
}
}
fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean {
val success = deleteFile(context, id)
if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString())
return success
}
private fun deleteFile(context: Context, id: Int): Boolean {
val info = context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return false
if (isScopedStorage()) {
val cr = context.contentResolver ?: return false
val fileUri =
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName)
?: return true // FILE NOT FOUND, ALREADY DELETED
return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0
} else {
val normalPath =
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${info.relativePath}${info.displayName}".replace(
'/',
File.separatorChar
)
val dFile = File(normalPath)
if (!dFile.exists()) return true
return dFile.delete()
}
}
fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? {
return context.getKey(KEY_RESUME_PACKAGES, id.toString())
}
fun downloadFromResume(context: Context, pkg: DownloadResumePackage) { fun downloadFromResume(context: Context, pkg: DownloadResumePackage) {
downloadQueue.addLast(pkg) downloadQueue.addLast(pkg)
downloadCheck(context) downloadCheck(context)