AquaStream/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt

1388 lines
49 KiB
Kotlin
Raw Normal View History

2021-06-29 23:14:48 +00:00
package com.lagradost.cloudstream3.utils
2021-09-19 22:46:05 +00:00
import android.app.Notification
2021-09-01 21:30:21 +00:00
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
2021-07-05 18:09:37 +00:00
import android.content.*
2021-06-29 23:14:48 +00:00
import android.graphics.Bitmap
2021-07-05 18:09:37 +00:00
import android.net.Uri
2021-06-29 23:14:48 +00:00
import android.os.Build
2021-07-04 17:00:04 +00:00
import android.os.Environment
2021-07-05 00:55:07 +00:00
import android.provider.MediaStore
2021-06-29 23:14:48 +00:00
import androidx.annotation.DrawableRes
2021-07-05 18:09:37 +00:00
import androidx.annotation.RequiresApi
2021-06-29 23:14:48 +00:00
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
2021-06-29 23:14:48 +00:00
import com.bumptech.glide.Glide
import com.fasterxml.jackson.annotation.JsonProperty
2021-06-29 23:14:48 +00:00
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
2021-07-04 17:00:04 +00:00
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.services.VideoDownloadService
2021-07-04 00:59:51 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.main
2021-07-05 20:28:50 +00:00
import com.lagradost.cloudstream3.utils.DataStore.getKey
2021-07-05 18:09:37 +00:00
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
2021-08-19 20:05:18 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
2021-07-04 00:59:51 +00:00
import kotlinx.coroutines.Dispatchers
2021-07-04 17:00:04 +00:00
import kotlinx.coroutines.delay
2021-07-04 00:59:51 +00:00
import kotlinx.coroutines.withContext
2021-07-05 18:09:37 +00:00
import java.io.*
2021-07-04 17:00:04 +00:00
import java.lang.Thread.sleep
import java.net.URI
2021-07-03 20:59:46 +00:00
import java.net.URL
import java.net.URLConnection
2021-07-04 17:00:04 +00:00
import java.util.*
2021-09-01 21:30:21 +00:00
import kotlin.math.roundToInt
2021-07-08 17:46:47 +00:00
2021-07-06 00:18:56 +00:00
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
const val DOWNLOAD_CHANNEL_NAME = "Downloads"
const val DOWNLOAD_CHANNEL_DESCRIPT = "The download notification channel"
2021-06-29 23:14:48 +00:00
object VideoDownloadManager {
2021-07-03 20:59:46 +00:00
var maxConcurrentDownloads = 3
2021-07-05 20:28:50 +00:00
private var currentDownloads = mutableListOf<Int>()
2021-07-03 20:59:46 +00:00
private const val USER_AGENT =
2021-08-14 17:31:27 +00:00
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
2021-07-03 20:59:46 +00:00
2021-06-29 23:14:48 +00:00
@DrawableRes
const val imgDone = R.drawable.rddone
@DrawableRes
const val imgDownloading = R.drawable.rdload
@DrawableRes
const val imgPaused = R.drawable.rdpause
@DrawableRes
const val imgStopped = R.drawable.rderror
@DrawableRes
const val imgError = R.drawable.rderror
@DrawableRes
const val pressToPauseIcon = R.drawable.ic_baseline_pause_24
@DrawableRes
const val pressToResumeIcon = R.drawable.ic_baseline_play_arrow_24
@DrawableRes
const val pressToStopIcon = R.drawable.exo_icon_stop
enum class DownloadType {
IsPaused,
IsDownloading,
IsDone,
IsFailed,
IsStopped,
}
enum class DownloadActionType {
Pause,
Resume,
Stop,
}
interface IDownloadableMinimum {
val url: String
val referer: String
val headers: Map<String, String>
}
2021-08-29 18:42:44 +00:00
fun IDownloadableMinimum.getId(): Int {
return url.hashCode()
}
2021-07-03 20:59:46 +00:00
data class DownloadEpisodeMetadata(
val id: Int,
val mainName: String,
val sourceApiName: String?,
val poster: String?,
val name: String?,
val season: Int?,
val episode: Int?
)
2021-07-04 17:00:04 +00:00
data class DownloadItem(
2021-07-06 00:18:56 +00:00
val source: String?,
2021-07-04 17:00:04 +00:00
val folder: String?,
val ep: DownloadEpisodeMetadata,
2021-09-01 21:30:21 +00:00
val links: List<ExtractorLink>,
2021-07-04 17:00:04 +00:00
)
2021-07-05 18:09:37 +00:00
data class DownloadResumePackage(
val item: DownloadItem,
val linkIndex: Int?,
)
2021-07-05 20:28:50 +00:00
data class DownloadedFileInfo(
val totalBytes: Long,
val relativePath: String,
val displayName: String,
2021-09-01 21:30:21 +00:00
val extraInfo: String? = null
2021-07-05 20:28:50 +00:00
)
data class DownloadedFileInfoResult(
2021-07-06 00:18:56 +00:00
val fileLength: Long,
2021-07-05 20:28:50 +00:00
val totalBytes: Long,
val path: Uri,
)
2021-07-08 17:46:47 +00:00
data class DownloadQueueResumePackage(
val index: Int,
val pkg: DownloadResumePackage,
)
2021-07-04 17:00:04 +00:00
private const val SUCCESS_DOWNLOAD_DONE = 1
2021-09-01 21:30:21 +00:00
private const val SUCCESS_STREAM = 3
2021-07-04 17:00:04 +00:00
private const val SUCCESS_STOPPED = 2
private const val ERROR_DELETING_FILE = 3 // will not download the next one, but is still classified as an error
2021-07-05 18:09:37 +00:00
private const val ERROR_CREATE_FILE = -2
2021-09-01 12:02:32 +00:00
private const val ERROR_UNKNOWN = -10
2021-07-04 17:00:04 +00:00
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
2021-07-05 18:09:37 +00:00
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
2021-07-04 17:00:04 +00:00
2021-08-29 18:42:44 +00:00
private const val KEY_RESUME_PACKAGES = "download_resume"
2021-07-08 17:46:47 +00:00
const val KEY_DOWNLOAD_INFO = "download_info"
2021-08-29 18:42:44 +00:00
private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume"
2021-07-04 17:00:04 +00:00
2021-07-06 00:18:56 +00:00
val downloadStatus = HashMap<Int, DownloadType>()
val downloadStatusEvent = Event<Pair<Int, DownloadType>>()
2021-07-25 14:25:09 +00:00
val downloadDeleteEvent = Event<Int>()
2021-07-05 20:28:50 +00:00
val downloadEvent = Event<Pair<Int, DownloadActionType>>()
2021-07-24 20:50:57 +00:00
val downloadProgressEvent = Event<Triple<Int, Long, Long>>()
2021-07-08 19:39:49 +00:00
val downloadQueue = LinkedList<DownloadResumePackage>()
2021-07-04 17:00:04 +00:00
2021-06-29 23:14:48 +00:00
private var hasCreatedNotChanel = false
private fun Context.createNotificationChannel() {
hasCreatedNotChanel = true
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
2021-07-06 00:18:56 +00:00
val name = DOWNLOAD_CHANNEL_NAME //getString(R.string.channel_name)
val descriptionText = DOWNLOAD_CHANNEL_DESCRIPT//getString(R.string.channel_description)
2021-06-29 23:14:48 +00:00
val importance = NotificationManager.IMPORTANCE_DEFAULT
2021-07-06 00:18:56 +00:00
val channel = NotificationChannel(DOWNLOAD_CHANNEL_ID, name, importance).apply {
2021-06-29 23:14:48 +00:00
description = descriptionText
}
// Register the channel with the system
val notificationManager: NotificationManager =
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
2021-07-24 15:13:21 +00:00
/** Will return IsDone if not found or error */
2021-07-24 15:35:53 +00:00
fun getDownloadState(id: Int): DownloadType {
2021-07-24 15:13:21 +00:00
return try {
downloadStatus[id] ?: DownloadType.IsDone
2021-07-24 15:35:53 +00:00
} catch (e: Exception) {
2021-09-01 13:18:41 +00:00
logError(e)
2021-07-24 15:13:21 +00:00
DownloadType.IsDone
}
}
2021-06-29 23:14:48 +00:00
private val cachedBitmaps = hashMapOf<String, Bitmap>()
private fun Context.getImageBitmapFromUrl(url: String): Bitmap? {
2021-08-19 20:05:18 +00:00
try {
if (cachedBitmaps.containsKey(url)) {
return cachedBitmaps[url]
}
2021-06-29 23:14:48 +00:00
val bitmap = GlideApp.with(this)
2021-08-19 20:05:18 +00:00
.asBitmap()
.load(url).into(720, 720)
.get()
if (bitmap != null) {
cachedBitmaps[url] = bitmap
}
return null
} catch (e: Exception) {
2021-08-19 20:05:18 +00:00
return null
2021-06-29 23:14:48 +00:00
}
}
suspend fun createNotification(
2021-06-29 23:14:48 +00:00
context: Context,
2021-07-03 20:59:46 +00:00
source: String?,
linkName: String?,
ep: DownloadEpisodeMetadata,
2021-06-29 23:14:48 +00:00
state: DownloadType,
progress: Long,
total: Long,
notificationCallback: (Int, Notification) -> Unit
): Notification? {
if (total <= 0) return null// crash, invalid data
// main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setAutoCancel(true)
.setColorized(true)
.setOnlyAlertOnce(true)
.setShowWhen(false)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(ep.mainName)
.setSmallIcon(
when (state) {
DownloadType.IsDone -> imgDone
DownloadType.IsDownloading -> imgDownloading
DownloadType.IsPaused -> imgPaused
DownloadType.IsFailed -> imgError
DownloadType.IsStopped -> imgStopped
}
)
2021-07-03 20:59:46 +00:00
if (ep.sourceApiName != null) {
builder.setSubText(ep.sourceApiName)
}
2021-06-29 23:14:48 +00:00
if (source != null) {
val intent = Intent(context, MainActivity::class.java).apply {
data = source.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
2021-07-04 17:00:04 +00:00
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
builder.setContentIntent(pendingIntent)
}
2021-06-29 23:14:48 +00:00
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false)
}
2021-07-03 20:59:46 +00:00
val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else ""
val rowTwo = if (ep.season != null && ep.episode != null) {
"${context.getString(R.string.season_short)}${ep.season}:${context.getString(R.string.episode_short)}${ep.episode}" + rowTwoExtra
} else if (ep.episode != null) {
"${context.getString(R.string.episode)} ${ep.episode}" + rowTwoExtra
} else {
(ep.name ?: "") + ""
}
val downloadFormat = context.getString(R.string.download_format)
2021-07-03 20:59:46 +00:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (ep.poster != null) {
val poster = withContext(Dispatchers.IO) {
context.getImageBitmapFromUrl(ep.poster)
2021-07-04 17:00:04 +00:00
}
if (poster != null)
builder.setLargeIcon(poster)
}
2021-07-03 20:59:46 +00:00
val progressPercentage = progress * 100 / total
val progressMbString = "%.1f".format(progress / 1000000f)
val totalMbString = "%.1f".format(total / 1000000f)
val bigText =
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
(if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString MB/$totalMbString MB)"
2021-07-03 20:59:46 +00:00
} else if (state == DownloadType.IsFailed) {
2021-09-02 16:51:13 +00:00
downloadFormat.format(context.getString(R.string.download_failed), rowTwo)
2021-07-03 20:59:46 +00:00
} else if (state == DownloadType.IsDone) {
2021-09-02 16:51:13 +00:00
downloadFormat.format(context.getString(R.string.download_done), rowTwo)
2021-07-03 20:59:46 +00:00
} else {
2021-09-02 16:51:13 +00:00
downloadFormat.format(context.getString(R.string.download_canceled), rowTwo)
2021-07-03 20:59:46 +00:00
}
val bodyStyle = NotificationCompat.BigTextStyle()
bodyStyle.bigText(bigText)
builder.setStyle(bodyStyle)
} else {
val txt = if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
rowTwo
} else if (state == DownloadType.IsFailed) {
downloadFormat.format(context.getString(R.string.download_failed), rowTwo)
} else if (state == DownloadType.IsDone) {
downloadFormat.format(context.getString(R.string.download_done), rowTwo)
} else {
downloadFormat.format(context.getString(R.string.download_canceled), rowTwo)
2021-07-03 20:59:46 +00:00
}
builder.setContentText(txt)
}
2021-06-29 23:14:48 +00:00
if ((state == DownloadType.IsDownloading || state == DownloadType.IsPaused) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val actionTypes: MutableList<DownloadActionType> = ArrayList()
// INIT
if (state == DownloadType.IsDownloading) {
actionTypes.add(DownloadActionType.Pause)
actionTypes.add(DownloadActionType.Stop)
}
2021-06-29 23:14:48 +00:00
if (state == DownloadType.IsPaused) {
actionTypes.add(DownloadActionType.Resume)
actionTypes.add(DownloadActionType.Stop)
}
2021-06-29 23:14:48 +00:00
// ADD ACTIONS
for ((index, i) in actionTypes.withIndex()) {
val actionResultIntent = Intent(context, VideoDownloadService::class.java)
2021-06-29 23:14:48 +00:00
actionResultIntent.putExtra(
"type", when (i) {
DownloadActionType.Resume -> "resume"
DownloadActionType.Pause -> "pause"
DownloadActionType.Stop -> "stop"
}
)
2021-06-29 23:14:48 +00:00
actionResultIntent.putExtra("id", ep.id)
2021-06-29 23:14:48 +00:00
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
context, (4337 + index * 100000 + ep.id),
actionResultIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
builder.addAction(
NotificationCompat.Action(
when (i) {
DownloadActionType.Resume -> pressToResumeIcon
DownloadActionType.Pause -> pressToPauseIcon
DownloadActionType.Stop -> pressToStopIcon
}, when (i) {
DownloadActionType.Resume -> context.getString(R.string.resume)
DownloadActionType.Pause -> context.getString(R.string.pause)
DownloadActionType.Stop -> context.getString(R.string.cancel)
}, pending
2021-06-29 23:14:48 +00:00
)
)
2021-06-29 23:14:48 +00:00
}
}
2021-06-29 23:14:48 +00:00
if (!hasCreatedNotChanel) {
context.createNotificationChannel()
}
2021-06-29 23:14:48 +00:00
val notification = builder.build()
notificationCallback(ep.id, notification)
with(NotificationManagerCompat.from(context)) {
// notificationId is a unique int for each notification that you must define
notify(ep.id, notification)
2021-06-29 23:14:48 +00:00
}
return notification
// }
2021-06-29 23:14:48 +00:00
}
2021-07-04 17:00:04 +00:00
private const val reservedChars = "|\\?*<\":>+[]/\'"
fun sanitizeFilename(name: String): String {
2021-07-04 00:59:51 +00:00
var tempName = name
for (c in reservedChars) {
tempName = tempName.replace(c, ' ')
}
2021-07-04 17:00:04 +00:00
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) {
return null
}
}
fun getFolder(context: Context, relativePath: String): List<Pair<String, Uri>>? {
if (isScopedStorage()) {
return context.contentResolver?.getExistingFolderStartName(relativePath)
} else {
val normalPath =
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace(
'/',
File.separatorChar
)
val folder = File(normalPath)
if (folder.isDirectory) {
2021-08-29 18:42:44 +00:00
return folder.listFiles()?.map { Pair(it.name, it.toUri()) }
}
return null
}
}
2021-07-05 18:09:37 +00:00
@RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
2021-08-09 14:40:20 +00:00
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
)
}
2021-07-05 18:09:37 +00:00
}
}
2021-08-09 14:40:20 +00:00
return null
} catch (e: Exception) {
return null
2021-07-05 18:09:37 +00:00
}
}
2021-07-05 20:28:50 +00:00
@RequiresApi(Build.VERSION_CODES.Q)
2021-08-09 14:40:20 +00:00
fun ContentResolver.getFileLength(fileUri: Uri): Long? {
return try {
this.openFileDescriptor(fileUri, "r")
.use { it?.statSize ?: 0 }
} catch (e: Exception) {
null
}
2021-07-05 20:28:50 +00:00
}
2021-07-05 18:09:37 +00:00
private fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
data class CreateNotificationMetadata(
val type: DownloadType,
val bytesDownloaded: Long,
val bytesTotal: Long,
)
2021-09-01 21:30:21 +00:00
data class StreamData(
val errorCode: Int,
val resume: Boolean? = null,
val fileLength: Long? = null,
val fileStream: OutputStream? = null,
)
private fun setupStream(
2021-08-30 17:11:04 +00:00
context: Context,
name: String,
folder: String?,
extension: String,
2021-09-01 21:30:21 +00:00
tryResume: Boolean,
): StreamData {
val relativePath = getRelativePath(folder)
val displayName = getDisplayName(name, extension)
2021-08-30 17:11:04 +00:00
val fileStream: OutputStream
val fileLength: Long
2021-09-01 21:30:21 +00:00
var resume = tryResume
2021-08-30 17:11:04 +00:00
if (isScopedStorage()) {
2021-09-01 21:30:21 +00:00
val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
2021-08-30 17:11:04 +00:00
val currentExistingFile =
cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
fileLength =
if (currentExistingFile == null || !resume) 0 else (cr.getFileLength(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) {
"vtt" -> "text/vtt"
"mp4" -> "video/mp4"
2021-09-16 12:46:21 +00:00
"srt" -> "application/x-subrip"//"text/plain"
2021-08-30 17:11:04 +00:00
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, relativePath)
}
cr.insert(
contentUri,
newFile
2021-09-01 21:30:21 +00:00
) ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
2021-08-30 17:11:04 +00:00
}
fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else ""))
2021-09-01 21:30:21 +00:00
?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
2021-08-30 17:11:04 +00:00
} else {
2021-09-01 21:30:21 +00:00
val normalPath = getNormalPath(relativePath, displayName)
2021-08-30 17:11:04 +00:00
// NORMAL NON SCOPED STORAGE FILE CREATION
val rFile = File(normalPath)
if (!rFile.exists()) {
fileLength = 0
rFile.parentFile?.mkdirs()
2021-09-01 21:30:21 +00:00
if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
2021-08-30 17:11:04 +00:00
} else {
if (resume) {
fileLength = rFile.length()
} else {
fileLength = 0
rFile.parentFile?.mkdirs()
2021-09-01 21:30:21 +00:00
if (!rFile.delete()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
2021-08-30 17:11:04 +00:00
}
}
fileStream = FileOutputStream(rFile, false)
}
2021-09-01 21:30:21 +00:00
if (fileLength == 0L) resume = false
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
}
fun downloadThing(
2021-07-03 20:59:46 +00:00
context: Context,
link: IDownloadableMinimum,
name: String,
2021-07-04 00:59:51 +00:00
folder: String?,
extension: String,
tryResume: Boolean,
parentId: Int?,
createNotificationCallback: (CreateNotificationMetadata) -> Unit
2021-07-04 17:00:04 +00:00
): Int {
2021-08-30 17:11:04 +00:00
if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) {
2021-09-19 22:46:05 +00:00
return ERROR_UNKNOWN
2021-08-30 17:11:04 +00:00
}
2021-09-01 21:30:21 +00:00
val relativePath = getRelativePath(folder)
val displayName = getDisplayName(name, extension)
2021-07-05 00:55:07 +00:00
2021-07-05 18:09:37 +00:00
fun deleteFile(): Int {
2021-09-01 21:30:21 +00:00
return delete(context, name, folder, extension, parentId)
2021-07-03 20:59:46 +00:00
}
2021-07-04 00:59:51 +00:00
2021-09-01 21:30:21 +00:00
val stream = setupStream(context, name, folder, extension, tryResume)
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
2021-07-04 00:59:51 +00:00
2021-09-01 21:30:21 +00:00
val resume = stream.resume!!
val fileStream = stream.fileStream!!
val fileLength = stream.fileLength!!
2021-07-03 20:59:46 +00:00
// CONNECT
val connection: URLConnection =
URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK
2021-07-03 20:59:46 +00:00
// SET CONNECTION SETTINGS
connection.connectTimeout = 10000
connection.setRequestProperty("Accept-Encoding", "identity")
2021-08-29 19:46:25 +00:00
connection.setRequestProperty("user-agent", USER_AGENT)
if (link.referer.isNotEmpty()) connection.setRequestProperty("referer", link.referer)
2021-08-14 17:31:27 +00:00
// extra stuff
connection.setRequestProperty(
"sec-ch-ua",
"\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\""
)
2021-08-29 19:46:25 +00:00
2021-08-14 17:31:27 +00:00
connection.setRequestProperty("sec-ch-ua-mobile", "?0")
2021-08-29 19:46:25 +00:00
connection.setRequestProperty("accept", "*/*")
2021-08-14 17:31:27 +00:00
// dataSource.setRequestProperty("Sec-Fetch-Site", "none") //same-site
2021-08-29 19:46:25 +00:00
connection.setRequestProperty("sec-fetch-user", "?1")
connection.setRequestProperty("sec-fetch-mode", "navigate")
connection.setRequestProperty("sec-fetch-dest", "video")
link.headers.entries.forEach {
connection.setRequestProperty(it.key, it.value)
}
2021-08-14 17:31:27 +00:00
2021-08-11 17:40:23 +00:00
if (resume)
connection.setRequestProperty("Range", "bytes=${fileLength}-")
2021-07-05 18:09:37 +00:00
val resumeLength = (if (resume) fileLength else 0)
2021-07-03 20:59:46 +00:00
// ON CONNECTION
connection.connect()
2021-08-11 17:40:23 +00:00
2021-09-01 12:02:32 +00:00
val contentLength = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // fuck android
2021-10-03 00:09:13 +00:00
connection.contentLengthLong
2021-09-01 12:02:32 +00:00
} else {
2021-10-03 00:09:13 +00:00
connection.getHeaderField("content-length").toLongOrNull() ?: connection.contentLength.toLong()
2021-09-01 12:02:32 +00:00
}
} catch (e: Exception) {
2021-09-01 13:18:41 +00:00
logError(e)
2021-09-01 12:02:32 +00:00
0L
2021-08-11 17:40:23 +00:00
}
2021-07-03 20:59:46 +00:00
val bytesTotal = contentLength + resumeLength
2021-08-11 17:40:23 +00:00
if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
2021-07-04 00:59:51 +00:00
parentId?.let {
context.setKey(
KEY_DOWNLOAD_INFO,
it.toString(),
DownloadedFileInfo(bytesTotal, relativePath, displayName)
)
}
2021-07-05 20:28:50 +00:00
2021-07-04 00:59:51 +00:00
// Could use connection.contentType for mime types when creating the file,
// however file is already created and players don't go of file type
// https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header
2021-07-04 17:00:04 +00:00
// might receive application/octet-stream
/*if (!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) {
return ERROR_WRONG_CONTENT // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE
}*/
2021-07-03 20:59:46 +00:00
// READ DATA FROM CONNECTION
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
val buffer = ByteArray(1024)
var count: Int
var bytesDownloaded = resumeLength
2021-07-04 17:00:04 +00:00
var isPaused = false
var isStopped = false
var isDone = false
var isFailed = false
2021-07-04 00:59:51 +00:00
// TO NOT REUSE CODE
2021-07-04 17:00:04 +00:00
fun updateNotification() {
val type = when {
isDone -> DownloadType.IsDone
isStopped -> DownloadType.IsStopped
isFailed -> DownloadType.IsFailed
isPaused -> DownloadType.IsPaused
else -> DownloadType.IsDownloading
}
2021-07-06 00:18:56 +00:00
parentId?.let { id ->
try {
downloadStatus[id] = type
downloadStatusEvent.invoke(Pair(id, type))
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal))
} catch (e: Exception) {
// IDK MIGHT ERROR
}
2021-07-06 00:18:56 +00:00
}
createNotificationCallback.invoke(CreateNotificationMetadata(type, bytesDownloaded, bytesTotal))
/*createNotification(
2021-07-03 20:59:46 +00:00
context,
source,
link.name,
ep,
type,
bytesDownloaded,
bytesTotal
)*/
2021-07-03 20:59:46 +00:00
}
val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
if (event.first == parentId) {
2021-07-04 17:00:04 +00:00
when (event.second) {
DownloadActionType.Pause -> {
isPaused = true; updateNotification()
}
DownloadActionType.Stop -> {
isStopped = true; updateNotification()
2021-07-17 21:36:50 +00:00
context.removeKey(KEY_RESUME_PACKAGES, event.first.toString())
saveQueue(context)
2021-07-04 17:00:04 +00:00
}
DownloadActionType.Resume -> {
isPaused = false; updateNotification()
}
}
}
}
if (parentId != null)
downloadEvent += downloadEventListener
2021-07-04 17:00:04 +00:00
// UPDATE DOWNLOAD NOTIFICATION
val notificationCoroutine = main {
while (true) {
if (!isPaused) {
updateNotification()
}
for (i in 1..10) {
delay(100)
}
}
2021-07-03 20:59:46 +00:00
}
2021-07-04 17:00:04 +00:00
// THE REAL READ
try {
while (true) {
count = connectionInputStream.read(buffer)
if (count < 0) break
bytesDownloaded += count
2021-07-24 15:13:21 +00:00
// downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) // Updates too much for any UI to keep up with
2021-07-04 17:00:04 +00:00
while (isPaused) {
sleep(100)
if (isStopped) {
break
}
}
if (isStopped) {
break
}
fileStream.write(buffer, 0, count)
}
} catch (e: Exception) {
isFailed = true
updateNotification()
}
// REMOVE AND EXIT ALL
2021-07-06 00:18:56 +00:00
fileStream.close()
connectionInputStream.close()
2021-07-04 17:00:04 +00:00
notificationCoroutine.cancel()
2021-07-03 20:59:46 +00:00
try {
if (parentId != null)
downloadEvent -= downloadEventListener
} catch (e: Exception) {
2021-09-01 13:18:41 +00:00
logError(e)
}
2021-07-06 00:18:56 +00:00
try {
parentId?.let {
downloadStatus.remove(it)
}
2021-07-06 00:18:56 +00:00
} catch (e: Exception) {
// IDK MIGHT ERROR
}
2021-07-04 17:00:04 +00:00
// RETURN MESSAGE
return when {
isFailed -> {
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
2021-07-04 17:00:04 +00:00
ERROR_CONNECTION_ERROR
}
isStopped -> {
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
2021-07-05 18:09:37 +00:00
deleteFile()
2021-07-04 17:00:04 +00:00
}
else -> {
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) }
2021-07-04 17:00:04 +00:00
isDone = true
updateNotification()
SUCCESS_DOWNLOAD_DONE
}
}
2021-06-29 23:14:48 +00:00
}
2021-09-01 21:30:21 +00:00
private fun getRelativePath(folder: String?): String {
return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
}
private fun getDisplayName(name: String, extension: String): String {
return "$name.$extension"
}
private fun getNormalPath(relativePath: String, displayName: String): String {
return "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
}
private fun delete(
context: Context,
name: String,
folder: String?,
extension: String,
parentId: Int?,
): Int {
val relativePath = getRelativePath(folder)
val displayName = getDisplayName(name, extension)
if (isScopedStorage()) {
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
if (lastContent != null) {
context.contentResolver.delete(lastContent, null, null)
}
} else {
if (!File(getNormalPath(relativePath, displayName)).delete()) return ERROR_DELETING_FILE
}
parentId?.let {
downloadDeleteEvent.invoke(parentId)
}
return SUCCESS_STOPPED
}
2021-09-01 12:21:03 +00:00
private fun downloadHLS(
context: Context,
link: ExtractorLink,
name: String,
folder: String?,
parentId: Int?,
2021-09-01 21:30:21 +00:00
startIndex: Int?,
2021-09-01 12:21:03 +00:00
createNotificationCallback: (CreateNotificationMetadata) -> Unit
): Int {
2021-09-01 21:30:21 +00:00
val extension = "mp4"
2021-09-01 12:21:03 +00:00
fun logcatPrint(vararg items: Any?) {
items.forEach {
println("[HLS]: $it")
}
}
val m3u8Helper = M3u8Helper()
2021-09-01 17:16:40 +00:00
logcatPrint("initialised the HLS downloader.")
2021-09-01 12:21:03 +00:00
2021-09-01 21:30:21 +00:00
val m3u8 = M3u8Helper.M3u8Stream(
link.url, when (link.quality) {
-2 -> 360
-1 -> 480
1 -> 720
2 -> 1080
else -> null
}, mapOf("referer" to link.referer)
)
2021-09-01 12:21:03 +00:00
2021-09-01 21:30:21 +00:00
var realIndex = startIndex ?: 0
val stream = setupStream(context, name, folder, extension, realIndex > 0)
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
2021-09-01 12:21:03 +00:00
2021-09-01 21:30:21 +00:00
if (!stream.resume!!) realIndex = 0
2021-09-04 13:24:37 +00:00
val fileLengthAdd = stream.fileLength!!
2021-09-01 21:30:21 +00:00
val tsIterator = m3u8Helper.hlsYield(listOf(m3u8), realIndex)
2021-09-01 12:21:03 +00:00
2021-09-01 21:30:21 +00:00
val relativePath = getRelativePath(folder)
val displayName = getDisplayName(name, extension)
2021-09-01 12:21:03 +00:00
2021-09-01 21:30:21 +00:00
val fileStream = stream.fileStream!!
2021-09-01 12:21:03 +00:00
val firstTs = tsIterator.next()
var isDone = false
var isFailed = false
2021-09-01 21:30:21 +00:00
var isPaused = false
2021-09-04 13:24:37 +00:00
var bytesDownloaded = firstTs.bytes.size.toLong() + fileLengthAdd
2021-09-01 21:30:21 +00:00
var tsProgress = 1L + realIndex
2021-09-01 12:21:03 +00:00
val totalTs = firstTs.totalTs.toLong()
2021-09-01 21:30:21 +00:00
fun deleteFile(): Int {
return delete(context, name, folder, extension, parentId)
}
2021-09-01 12:21:03 +00:00
/*
Most of the auto generated m3u8 out there have TS of the same size.
And only the last TS might have a different size.
But oh well, in cases of handmade m3u8 streams this will go all over the place ¯\_()_/¯
So ya, this calculates an estimate of how many bytes the file is going to be.
> (bytesDownloaded/tsProgress)*totalTs
*/
2021-09-01 21:30:21 +00:00
fun updateInfo() {
parentId?.let {
context.setKey(
KEY_DOWNLOAD_INFO,
it.toString(),
DownloadedFileInfo(
(bytesDownloaded / tsProgress) * totalTs,
relativePath,
displayName,
tsProgress.toString()
)
)
}
2021-09-01 12:21:03 +00:00
}
2021-09-01 21:30:21 +00:00
updateInfo()
2021-09-01 12:21:03 +00:00
fun updateNotification() {
val type = when {
isDone -> DownloadType.IsDone
isFailed -> DownloadType.IsFailed
2021-09-01 21:30:21 +00:00
isPaused -> DownloadType.IsPaused
2021-09-01 12:21:03 +00:00
else -> DownloadType.IsDownloading
}
parentId?.let { id ->
try {
downloadStatus[id] = type
downloadStatusEvent.invoke(Pair(id, type))
downloadProgressEvent.invoke(
Triple(
id,
bytesDownloaded,
(bytesDownloaded / tsProgress) * totalTs
)
)
2021-09-01 12:21:03 +00:00
} catch (e: Exception) {
// IDK MIGHT ERROR
}
}
2021-09-01 21:30:21 +00:00
createNotificationCallback.invoke(
CreateNotificationMetadata(
type,
bytesDownloaded,
(bytesDownloaded / tsProgress) * totalTs
)
)
2021-09-01 12:21:03 +00:00
}
2021-09-01 17:16:40 +00:00
fun stopIfError(ts: M3u8Helper.HlsDownloadData): Int? {
if (ts.errored || ts.bytes.isEmpty()) {
2021-09-01 21:30:21 +00:00
val error: Int = if (!ts.errored) {
2021-09-01 17:16:40 +00:00
logcatPrint("Error: No stream was found.")
ERROR_UNKNOWN
} else {
logcatPrint("Error: Failed to fetch data.")
ERROR_CONNECTION_ERROR
}
isFailed = true
fileStream.close()
deleteFile()
updateNotification()
return error
}
return null
2021-09-01 12:21:03 +00:00
}
val notificationCoroutine = main {
while (true) {
if (!isDone) {
updateNotification()
}
for (i in 1..10) {
delay(100)
}
}
}
val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
if (event.first == parentId) {
when (event.second) {
DownloadActionType.Stop -> {
isFailed = true
}
DownloadActionType.Pause -> {
2021-09-01 21:30:21 +00:00
isPaused =
true // Pausing is not supported since well...I need to know the index of the ts it was paused at
2021-09-01 12:21:03 +00:00
// it may be possible to store it in a variable, but when the app restarts it will be lost
}
2021-09-01 21:30:21 +00:00
DownloadActionType.Resume -> {
isPaused = false
}
2021-09-01 12:21:03 +00:00
}
2021-09-01 21:30:21 +00:00
updateNotification()
2021-09-01 12:21:03 +00:00
}
}
fun closeAll() {
try {
if (parentId != null)
downloadEvent -= downloadEventListener
} catch (e: Exception) {
logError(e)
}
try {
parentId?.let {
downloadStatus.remove(it)
}
} catch (e: Exception) {
logError(e)
// IDK MIGHT ERROR
}
notificationCoroutine.cancel()
}
2021-09-01 21:30:21 +00:00
stopIfError(firstTs).let {
2021-09-01 17:16:40 +00:00
if (it != null) {
closeAll()
return it
}
}
2021-09-01 12:21:03 +00:00
if (parentId != null)
downloadEvent += downloadEventListener
fileStream.write(firstTs.bytes)
2021-09-01 21:30:21 +00:00
fun onFailed() {
fileStream.close()
deleteFile()
updateNotification()
closeAll()
}
2021-09-01 12:21:03 +00:00
for (ts in tsIterator) {
2021-09-01 21:30:21 +00:00
while (isPaused) {
if (isFailed) {
onFailed()
return SUCCESS_STOPPED
}
sleep(100)
}
2021-09-01 12:21:03 +00:00
if (isFailed) {
2021-09-01 21:30:21 +00:00
onFailed()
2021-09-01 12:21:03 +00:00
return SUCCESS_STOPPED
}
2021-09-01 21:30:21 +00:00
2021-09-01 17:16:40 +00:00
stopIfError(ts).let {
if (it != null) {
closeAll()
return it
}
2021-09-01 12:21:03 +00:00
}
2021-09-01 17:16:40 +00:00
2021-09-01 12:21:03 +00:00
fileStream.write(ts.bytes)
2021-09-01 17:16:40 +00:00
tsProgress = ts.currentIndex.toLong()
2021-09-01 12:21:03 +00:00
bytesDownloaded += ts.bytes.size.toLong()
2021-09-01 21:30:21 +00:00
logcatPrint("Download progress ${((tsProgress.toFloat() / totalTs.toFloat()) * 100).roundToInt()}%")
updateInfo()
2021-09-01 12:21:03 +00:00
}
isDone = true
fileStream.close()
updateNotification()
closeAll()
2021-09-01 21:30:21 +00:00
updateInfo()
2021-09-01 12:21:03 +00:00
return SUCCESS_DOWNLOAD_DONE
}
2021-09-01 21:30:21 +00:00
private fun downloadSingleEpisode(
context: Context,
source: String?,
folder: String?,
ep: DownloadEpisodeMetadata,
link: ExtractorLink,
notificationCallback: (Int, Notification) -> Unit,
tryResume: Boolean = false,
): Int {
2021-09-02 16:51:13 +00:00
val name = sanitizeFilename(ep.name ?: "${context.getString(R.string.episode)} ${ep.episode}")
if (link.isM3u8 || URI(link.url).path.endsWith(".m3u8")) {
2021-09-01 21:30:21 +00:00
val startIndex = if (tryResume) {
context.getKey<DownloadedFileInfo>(
KEY_DOWNLOAD_INFO,
ep.id.toString(),
null
)?.extraInfo?.toIntOrNull()
2021-09-01 21:30:21 +00:00
} else null
return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta ->
main {
createNotification(
context,
source,
link.name,
ep,
meta.type,
meta.bytesDownloaded,
meta.bytesTotal,
notificationCallback
)
}
2021-09-01 12:21:03 +00:00
}
}
2021-09-01 12:02:32 +00:00
return normalSafeApiCall {
downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta ->
main {
createNotification(
context,
source,
link.name,
ep,
meta.type,
meta.bytesDownloaded,
meta.bytesTotal,
notificationCallback
)
}
2021-09-01 12:02:32 +00:00
}
2021-09-01 12:02:32 +00:00
} ?: ERROR_UNKNOWN
}
fun downloadCheck(
context: Context, notificationCallback: (Int, Notification) -> Unit,
): Int? {
2021-07-05 20:28:50 +00:00
if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
2021-07-05 18:09:37 +00:00
val pkg = downloadQueue.removeFirst()
val item = pkg.item
2021-07-05 20:28:50 +00:00
val id = item.ep.id
if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT
downloadEvent.invoke(Pair(id, DownloadActionType.Resume))
/** ID needs to be returned to the work-manager to properly await notification */
return id
2021-07-05 20:28:50 +00:00
}
2021-07-08 17:46:47 +00:00
2021-07-05 20:28:50 +00:00
currentDownloads.add(id)
2021-07-05 18:43:28 +00:00
main {
try {
2021-07-05 18:09:37 +00:00
for (index in (pkg.linkIndex ?: 0) until item.links.size) {
val link = item.links[index]
val resume = pkg.linkIndex == index
2021-07-05 20:28:50 +00:00
context.setKey(KEY_RESUME_PACKAGES, id.toString(), DownloadResumePackage(item, index))
2021-07-04 17:00:04 +00:00
val connectionResult = withContext(Dispatchers.IO) {
normalSafeApiCall {
downloadSingleEpisode(
context,
item.source,
item.folder,
item.ep,
link,
notificationCallback,
resume
)
2021-07-04 17:00:04 +00:00
}
}
if (connectionResult != null && connectionResult > 0) { // SUCCESS
2021-07-05 20:28:50 +00:00
context.removeKey(KEY_RESUME_PACKAGES, id.toString())
2021-07-04 17:00:04 +00:00
break
}
}
2021-07-05 18:43:28 +00:00
} catch (e: Exception) {
logError(e)
} finally {
2021-07-05 20:28:50 +00:00
currentDownloads.remove(id)
// Because otherwise notifications will not get caught by the workmanager
downloadCheckUsingWorker(context)
2021-07-04 17:00:04 +00:00
}
}
}
return null
2021-07-04 17:00:04 +00:00
}
2021-07-05 20:28:50 +00:00
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
2021-08-09 14:40:20 +00:00
val fileLength = cr.getFileLength(fileUri) ?: return null
2021-07-05 20:28:50 +00:00
if (fileLength == 0L) return null
2021-07-06 00:18:56 +00:00
return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri)
2021-07-05 20:28:50 +00:00
} else {
2021-09-01 21:30:21 +00:00
val normalPath = getNormalPath(info.relativePath, info.displayName)
2021-07-05 20:28:50 +00:00
val dFile = File(normalPath)
if (!dFile.exists()) return null
2021-07-06 00:18:56 +00:00
return DownloadedFileInfoResult(dFile.length(), info.totalBytes, dFile.toUri())
2021-07-05 20:28:50 +00:00
}
}
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 {
2021-07-25 14:25:09 +00:00
val info = context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return false
2021-07-24 15:35:53 +00:00
downloadEvent.invoke(Pair(id, DownloadActionType.Stop))
2021-07-24 20:50:57 +00:00
downloadProgressEvent.invoke(Triple(id, 0, 0))
downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped))
2021-07-25 14:25:09 +00:00
downloadDeleteEvent.invoke(id)
2021-07-05 20:28:50 +00:00
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 {
2021-09-01 21:30:21 +00:00
val normalPath = getNormalPath(info.relativePath, info.displayName)
2021-07-05 20:28:50 +00:00
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,
notificationCallback: (Int, Notification) -> Unit,
setKey: Boolean = true
) {
2021-07-17 15:56:26 +00:00
if (!currentDownloads.any { it == pkg.item.ep.id }) {
if (currentDownloads.size == maxConcurrentDownloads) {
main {
// showToast( // can be replaced with regular Toast
// context,
// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${
// context.getString(
// R.string.queued
// )
// }",
// Toast.LENGTH_SHORT
// )
2021-07-17 15:56:26 +00:00
}
}
downloadQueue.addLast(pkg)
downloadCheck(context, notificationCallback)
2021-07-17 15:56:26 +00:00
if (setKey) saveQueue(context)
2021-07-24 15:13:21 +00:00
} else {
downloadEvent.invoke(
Pair(pkg.item.ep.id, DownloadActionType.Resume)
)
2021-07-17 15:56:26 +00:00
}
}
private fun saveQueue(context: Context) {
val dQueue =
downloadQueue.toList().mapIndexed { index, any -> DownloadQueueResumePackage(index, any) }
.toTypedArray()
context.setKey(KEY_RESUME_QUEUE_PACKAGES, dQueue)
2021-07-05 18:09:37 +00:00
}
2021-08-29 18:42:44 +00:00
/*fun isMyServiceRunning(context: Context, serviceClass: Class<*>): Boolean {
2021-07-08 17:46:47 +00:00
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager?
for (service in manager!!.getRunningServices(Int.MAX_VALUE)) {
if (serviceClass.name == service.service.className) {
return true
}
}
return false
2021-08-29 18:42:44 +00:00
}*/
2021-07-08 17:46:47 +00:00
2021-07-04 17:00:04 +00:00
fun downloadEpisode(
context: Context?,
2021-07-06 00:18:56 +00:00
source: String?,
2021-07-04 00:59:51 +00:00
folder: String?,
ep: DownloadEpisodeMetadata,
links: List<ExtractorLink>,
notificationCallback: (Int, Notification) -> Unit,
2021-07-04 00:59:51 +00:00
) {
2021-08-29 19:46:25 +00:00
if (context == null) return
2021-09-01 12:21:03 +00:00
if (links.isNotEmpty()) {
downloadFromResume(
context,
DownloadResumePackage(DownloadItem(source, folder, ep, links), null),
notificationCallback
)
2021-06-29 23:14:48 +00:00
}
}
/** Worker stuff */
private fun startWork(context: Context, key: String) {
val req = OneTimeWorkRequest.Builder(DownloadFileWorkManager::class.java)
.setInputData(
Data.Builder()
.putString("key", key)
.build()
)
.build()
(WorkManager.getInstance(context)).enqueueUniqueWork(
key,
ExistingWorkPolicy.KEEP,
req
)
}
fun downloadCheckUsingWorker(
context: Context,
) {
startWork(context, DOWNLOAD_CHECK)
}
fun downloadFromResumeUsingWorker(
context: Context,
pkg: DownloadResumePackage,
) {
val key = pkg.item.ep.id.toString()
context.setKey(WORK_KEY_PACKAGE, key, pkg)
startWork(context, key)
}
// Keys are needed to transfer the data to the worker reliably and without exceeding the data limit
const val WORK_KEY_PACKAGE = "work_key_package"
const val WORK_KEY_INFO = "work_key_info"
fun downloadEpisodeUsingWorker(
context: Context,
source: String?,
folder: String?,
ep: DownloadEpisodeMetadata,
links: List<ExtractorLink>,
) {
val info = DownloadInfo(
source, folder, ep, links
)
val key = info.ep.id.toString()
context.setKey(WORK_KEY_INFO, key, info)
startWork(context, key)
}
data class DownloadInfo(
@JsonProperty("source") val source: String?,
@JsonProperty("folder") val folder: String?,
@JsonProperty("ep") val ep: DownloadEpisodeMetadata,
@JsonProperty("links") val links: List<ExtractorLink>
)
2021-09-01 12:21:03 +00:00
}