mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	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 | ||||
| 
 | ||||
|         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 | ||||
|         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 | ||||
|         } | ||||
| 
 | ||||
|         val dFile = | ||||
|             if (resume) tempFile | ||||
|             else DocumentFileCompat.createFile(context, basePath = path, mimeType = "video/mp4") | ||||
|         if (isScopedStorage()) { | ||||
|             val cr = context.contentResolver ?: return ERROR_CONTENT_RESOLVER_NOT_FOUND | ||||
| 
 | ||||
|         // END OF FILE CREATION | ||||
|             val currentExistingFile = cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH | ||||
| 
 | ||||
|         if (dFile == null || !dFile.exists()) { | ||||
|             return ERROR_FILE_NOT_FOUND | ||||
|             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.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) | ||||
|         } | ||||
| 
 | ||||
|         // OPEN FILE | ||||
|         val fileStream = dFile.openOutputStream(context, resume) ?: return ERROR_OPEN_FILE | ||||
|         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…
	
	Add table
		Add a link
		
	
		Reference in a new issue