forked from recloudstream/cloudstream
download manager finished
This commit is contained in:
parent
98185c0763
commit
d4c0c17859
1 changed files with 146 additions and 55 deletions
|
@ -3,21 +3,18 @@ package com.lagradost.cloudstream3.utils
|
|||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
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.lagradost.cloudstream3.MainActivity
|
||||
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.services.VideoDownloadService
|
||||
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.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.*
|
||||
import java.lang.Thread.sleep
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
@ -103,18 +101,27 @@ object VideoDownloadManager {
|
|||
val links: List<ExtractorLink>
|
||||
)
|
||||
|
||||
data class DownloadResumePackage(
|
||||
val item: DownloadItem,
|
||||
val linkIndex: Int?,
|
||||
)
|
||||
|
||||
private const val SUCCESS_DOWNLOAD_DONE = 1
|
||||
private const val SUCCESS_STOPPED = 2
|
||||
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_TOO_SMALL_CONNECTION = -4
|
||||
private const val ERROR_WRONG_CONTENT = -5
|
||||
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>>()
|
||||
private val downloadQueue = LinkedList<DownloadItem>()
|
||||
|
||||
private val downloadQueue = LinkedList<DownloadResumePackage>()
|
||||
|
||||
private var hasCreatedNotChanel = false
|
||||
private fun Context.createNotificationChannel() {
|
||||
|
@ -323,68 +330,143 @@ object VideoDownloadManager {
|
|||
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(
|
||||
context: Context,
|
||||
source: String?,
|
||||
folder: String?,
|
||||
ep: DownloadEpisodeMetadata,
|
||||
link: ExtractorLink
|
||||
link: ExtractorLink,
|
||||
tryResume: Boolean = false,
|
||||
): Int {
|
||||
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) {
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
|
||||
var resume = tryResume
|
||||
|
||||
} else {
|
||||
TODO("VERSION.SDK_INT < Q")
|
||||
val fileStream: OutputStream
|
||||
val fileLength: Long
|
||||
|
||||
fun deleteFile(): Int {
|
||||
if (isScopedStorage()) {
|
||||
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
|
||||
if (lastContent != null) {
|
||||
context.contentResolver.delete(lastContent, null, null)
|
||||
}
|
||||
} else {
|
||||
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
|
||||
}
|
||||
return SUCCESS_STOPPED
|
||||
}
|
||||
|
||||
if (isScopedStorage()) {
|
||||
val cr = context.contentResolver ?: return ERROR_CONTENT_RESOLVER_NOT_FOUND
|
||||
|
||||
val currentExistingFile = cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
|
||||
|
||||
fileLength =
|
||||
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.Downloads.DISPLAY_NAME, "$name.mp4")
|
||||
// put(MediaStore.Downloads.RELATIVE_PATH, if (folder == null) "" else "$folder")
|
||||
}
|
||||
val newFileUri = context.contentResolver.insert(collection, newFile) ?: throw Exception("FUCK YOU")
|
||||
val outputStream = context.contentResolver.openOutputStream(newFileUri, "w")
|
||||
?: throw Exception("ContentResolver couldn't open $newFileUri outputStream")
|
||||
return 0
|
||||
// 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
|
||||
}
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.MediaColumns.TITLE, name)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||
put(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
relativePath
|
||||
)
|
||||
}
|
||||
|
||||
val dFile =
|
||||
if (resume) tempFile
|
||||
else DocumentFileCompat.createFile(context, basePath = path, mimeType = "video/mp4")
|
||||
|
||||
// END OF FILE CREATION
|
||||
|
||||
if (dFile == null || !dFile.exists()) {
|
||||
return ERROR_FILE_NOT_FOUND
|
||||
cr.insert(
|
||||
contentUri,
|
||||
newFile
|
||||
) ?: return ERROR_MEDIA_STORE_URI_CANT_BE_CREATED
|
||||
}
|
||||
|
||||
// OPEN FILE
|
||||
val fileStream = dFile.openOutputStream(context, resume) ?: return ERROR_OPEN_FILE
|
||||
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
|
||||
|
||||
// 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
|
||||
connection.connectTimeout = 10000
|
||||
connection.setRequestProperty("Accept-Encoding", "identity")
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT)
|
||||
if (link.referer.isNotEmpty()) connection.setRequestProperty("Referer", link.referer)
|
||||
if (resume) connection.setRequestProperty("Range", "bytes=${dFile.length()}-")
|
||||
val resumeLength = (if (resume) dFile.length() else 0)
|
||||
if (resume) connection.setRequestProperty("Range", "bytes=${fileLength}-")
|
||||
val resumeLength = (if (resume) fileLength else 0)
|
||||
|
||||
// ON CONNECTION
|
||||
connection.connect()
|
||||
|
@ -497,8 +579,7 @@ object VideoDownloadManager {
|
|||
ERROR_CONNECTION_ERROR
|
||||
}
|
||||
isStopped -> {
|
||||
dFile.delete()
|
||||
SUCCESS_STOPPED
|
||||
deleteFile()
|
||||
}
|
||||
else -> {
|
||||
isDone = true
|
||||
|
@ -510,17 +591,23 @@ object VideoDownloadManager {
|
|||
|
||||
private fun downloadCheck(context: Context) {
|
||||
if (currentDownloads < maxConcurrentDownloads && downloadQueue.size > 0) {
|
||||
val item = downloadQueue.removeFirst()
|
||||
val pkg = downloadQueue.removeFirst()
|
||||
val item = pkg.item
|
||||
currentDownloads++
|
||||
try {
|
||||
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) {
|
||||
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
|
||||
context.removeKey(KEY_RESUME_STORAGE, item.ep.id.toString())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -534,6 +621,11 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
fun downloadFromResume(context: Context, pkg: DownloadResumePackage) {
|
||||
downloadQueue.addLast(pkg)
|
||||
downloadCheck(context)
|
||||
}
|
||||
|
||||
fun downloadEpisode(
|
||||
context: Context,
|
||||
source: String,
|
||||
|
@ -543,8 +635,7 @@ object VideoDownloadManager {
|
|||
) {
|
||||
val validLinks = links.filter { !it.isM3u8 }
|
||||
if (validLinks.isNotEmpty()) {
|
||||
downloadQueue.addLast(DownloadItem(source, folder, ep, validLinks))
|
||||
downloadCheck(context)
|
||||
downloadFromResume(context, DownloadResumePackage(DownloadItem(source, folder, ep, validLinks), null))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue