download manager finished

This commit is contained in:
LagradOst 2021-07-05 20:09:37 +02:00
parent 98185c0763
commit d4c0c17859

View file

@ -3,21 +3,18 @@ package com.lagradost.cloudstream3.utils
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ContentValues import android.content.*
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
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 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
import com.anggrayudi.storage.extension.closeStream import com.anggrayudi.storage.extension.closeStream
import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
@ -26,11 +23,12 @@ 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.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedInputStream import java.io.*
import java.io.InputStream
import java.lang.Thread.sleep import java.lang.Thread.sleep
import java.net.URL import java.net.URL
import java.net.URLConnection import java.net.URLConnection
@ -103,18 +101,27 @@ object VideoDownloadManager {
val links: List<ExtractorLink> val links: List<ExtractorLink>
) )
data class DownloadResumePackage(
val item: DownloadItem,
val linkIndex: Int?,
)
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
private const val ERROR_FILE_NOT_FOUND = -2 private const val ERROR_CREATE_FILE = -2
private const val ERROR_OPEN_FILE = -3 private const val ERROR_OPEN_FILE = -3
private const val ERROR_TOO_SMALL_CONNECTION = -4 private const val ERROR_TOO_SMALL_CONNECTION = -4
private const val ERROR_WRONG_CONTENT = -5 private const val ERROR_WRONG_CONTENT = -5
private const val ERROR_CONNECTION_ERROR = -6 private const val ERROR_CONNECTION_ERROR = -6
private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7
private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8
private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9
private const val KEY_RESUME_STORAGE = "download_resume"
val events = Event<Pair<Int, DownloadActionType>>() val events = Event<Pair<Int, DownloadActionType>>()
private val downloadQueue = LinkedList<DownloadItem>() private val downloadQueue = LinkedList<DownloadResumePackage>()
private var hasCreatedNotChanel = false private var hasCreatedNotChanel = false
private fun Context.createNotificationChannel() { private fun Context.createNotificationChannel() {
@ -323,68 +330,143 @@ object VideoDownloadManager {
return tempName.replace(" ", " ").trim(' ') return tempName.replace(" ", " ").trim(' ')
} }
@RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
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
}
private fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
private fun downloadSingleEpisode( private fun downloadSingleEpisode(
context: Context, context: Context,
source: String?, source: String?,
folder: String?, folder: String?,
ep: DownloadEpisodeMetadata, ep: DownloadEpisodeMetadata,
link: ExtractorLink link: ExtractorLink,
tryResume: Boolean = false,
): Int { ): Int {
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}") val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
val path = "${if (folder == null) "" else "$folder/"}$name.mp4" //${Environment.DIRECTORY_DOWNLOADS}/
var resume = false
val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
val displayName = "$name.mp4"
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) var resume = tryResume
} else { val fileStream: OutputStream
TODO("VERSION.SDK_INT < Q") val fileLength: Long
}
val newFile = ContentValues().apply { fun deleteFile(): Int {
put(MediaStore.Downloads.DISPLAY_NAME, "$name.mp4") if (isScopedStorage()) {
// put(MediaStore.Downloads.RELATIVE_PATH, if (folder == null) "" else "$folder") val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
} if (lastContent != null) {
val newFileUri = context.contentResolver.insert(collection, newFile) ?: throw Exception("FUCK YOU") context.contentResolver.delete(lastContent, null, null)
val outputStream = context.contentResolver.openOutputStream(newFileUri, "w") }
?: throw Exception("ContentResolver couldn't open $newFileUri outputStream") } else {
return 0 if (!File(normalPath).delete()) return ERROR_DELETING_FILE
// IF RESUME, DON'T DELETE FILE, CONTINUE, RECREATE IF NOT FOUND
// IF NOT RESUME CREATE FILE
val tempFile = DocumentFileCompat.fromSimplePath(context, basePath = path)
val fileExists = tempFile?.exists() ?: false
if (!fileExists) resume = false
if (fileExists && !resume) {
if (tempFile?.delete() == false) { // DELETE FAILED ON RESUME FILE
return ERROR_DELETING_FILE
} }
return SUCCESS_STOPPED
} }
val dFile = if (isScopedStorage()) {
if (resume) tempFile val cr = context.contentResolver ?: return ERROR_CONTENT_RESOLVER_NOT_FOUND
else DocumentFileCompat.createFile(context, basePath = path, mimeType = "video/mp4")
// END OF FILE CREATION val currentExistingFile = cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
if (dFile == null || !dFile.exists()) { fileLength =
return ERROR_FILE_NOT_FOUND if (currentExistingFile == null || !resume) 0 else cr.openFileDescriptor(currentExistingFile, "r")
.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
val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null)
if (rowsDeleted < 1) {
println("ERROR DELETING FILE!!!")
}
}
val newFileUri = if (resume && currentExistingFile != null) currentExistingFile else {
val contentUri =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
put(
MediaStore.MediaColumns.RELATIVE_PATH,
relativePath
)
}
cr.insert(
contentUri,
newFile
) ?: return ERROR_MEDIA_STORE_URI_CANT_BE_CREATED
}
fileStream = cr.openOutputStream(newFileUri, "w")
?: return ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM
} else {
// NORMAL NON SCOPED STORAGE FILE CREATION
val rFile = File(normalPath)
if (!rFile.exists()) {
fileLength = 0
rFile.parentFile?.mkdirs()
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
} else {
if (resume) {
fileLength = rFile.length()
} else {
fileLength = 0
rFile.parentFile?.mkdirs()
if (!rFile.delete()) return ERROR_DELETING_FILE
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
}
}
fileStream = FileOutputStream(rFile, false)
} }
if (fileLength == 0L) resume = false
// OPEN FILE
val fileStream = dFile.openOutputStream(context, resume) ?: return ERROR_OPEN_FILE
// CONNECT // CONNECT
val connection: URLConnection = URL(link.url).openConnection() val connection: URLConnection = URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK
// SET CONNECTION SETTINGS // SET CONNECTION SETTINGS
connection.connectTimeout = 10000 connection.connectTimeout = 10000
connection.setRequestProperty("Accept-Encoding", "identity") connection.setRequestProperty("Accept-Encoding", "identity")
connection.setRequestProperty("User-Agent", USER_AGENT) connection.setRequestProperty("User-Agent", USER_AGENT)
if (link.referer.isNotEmpty()) connection.setRequestProperty("Referer", link.referer) if (link.referer.isNotEmpty()) connection.setRequestProperty("Referer", link.referer)
if (resume) connection.setRequestProperty("Range", "bytes=${dFile.length()}-") if (resume) connection.setRequestProperty("Range", "bytes=${fileLength}-")
val resumeLength = (if (resume) dFile.length() else 0) val resumeLength = (if (resume) fileLength else 0)
// ON CONNECTION // ON CONNECTION
connection.connect() connection.connect()
@ -497,8 +579,7 @@ object VideoDownloadManager {
ERROR_CONNECTION_ERROR ERROR_CONNECTION_ERROR
} }
isStopped -> { isStopped -> {
dFile.delete() deleteFile()
SUCCESS_STOPPED
} }
else -> { else -> {
isDone = true isDone = true
@ -510,17 +591,23 @@ object VideoDownloadManager {
private fun downloadCheck(context: Context) { private fun downloadCheck(context: Context) {
if (currentDownloads < maxConcurrentDownloads && downloadQueue.size > 0) { if (currentDownloads < maxConcurrentDownloads && downloadQueue.size > 0) {
val item = downloadQueue.removeFirst() val pkg = downloadQueue.removeFirst()
val item = pkg.item
currentDownloads++ currentDownloads++
try { try {
main { main {
for (link in item.links) { for (index in (pkg.linkIndex ?: 0) until item.links.size) {
val link = item.links[index]
val resume = pkg.linkIndex == index
context.setKey(KEY_RESUME_STORAGE, item.ep.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) 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())
break break
} }
} }
@ -534,6 +621,11 @@ object VideoDownloadManager {
} }
} }
fun downloadFromResume(context: Context, pkg: DownloadResumePackage) {
downloadQueue.addLast(pkg)
downloadCheck(context)
}
fun downloadEpisode( fun downloadEpisode(
context: Context, context: Context,
source: String, source: String,
@ -543,8 +635,7 @@ object VideoDownloadManager {
) { ) {
val validLinks = links.filter { !it.isM3u8 } val validLinks = links.filter { !it.isM3u8 }
if (validLinks.isNotEmpty()) { if (validLinks.isNotEmpty()) {
downloadQueue.addLast(DownloadItem(source, folder, ep, validLinks)) downloadFromResume(context, DownloadResumePackage(DownloadItem(source, folder, ep, validLinks), null))
downloadCheck(context)
} }
} }
} }