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
|
2021-11-01 15:33:46 +00:00
|
|
|
import androidx.preference.PreferenceManager
|
2021-09-14 22:18:03 +00:00
|
|
|
import androidx.work.Data
|
|
|
|
import androidx.work.ExistingWorkPolicy
|
|
|
|
import androidx.work.OneTimeWorkRequest
|
|
|
|
import androidx.work.WorkManager
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
2021-11-01 15:33:46 +00:00
|
|
|
import com.hippo.unifile.UniFile
|
2022-02-25 23:04:00 +00:00
|
|
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
2021-12-16 23:45:20 +00:00
|
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
2021-06-29 23:14:48 +00:00
|
|
|
import com.lagradost.cloudstream3.MainActivity
|
|
|
|
import com.lagradost.cloudstream3.R
|
2022-04-03 20:14:51 +00:00
|
|
|
import com.lagradost.cloudstream3.TvType
|
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
|
2022-02-25 23:04:00 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
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
|
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
|
2022-07-30 15:43:37 +00:00
|
|
|
import kotlinx.coroutines.runBlocking
|
2021-07-04 00:59:51 +00:00
|
|
|
import kotlinx.coroutines.withContext
|
2021-11-02 14:25:12 +00:00
|
|
|
import okhttp3.internal.closeQuietly
|
2021-11-06 21:06:13 +00:00
|
|
|
import java.io.BufferedInputStream
|
|
|
|
import java.io.File
|
|
|
|
import java.io.InputStream
|
|
|
|
import java.io.OutputStream
|
2021-07-04 17:00:04 +00:00
|
|
|
import java.lang.Thread.sleep
|
2021-10-03 20:23:06 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
interface IDownloadableMinimum {
|
|
|
|
val url: String
|
|
|
|
val referer: String
|
2021-09-11 18:44:37 +00:00
|
|
|
val headers: Map<String, String>
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-08-29 18:42:44 +00:00
|
|
|
fun IDownloadableMinimum.getId(): Int {
|
2021-08-22 17:14:48 +00:00
|
|
|
return url.hashCode()
|
|
|
|
}
|
|
|
|
|
2021-07-03 20:59:46 +00:00
|
|
|
data class DownloadEpisodeMetadata(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("id") val id: Int,
|
|
|
|
@JsonProperty("mainName") val mainName: String,
|
|
|
|
@JsonProperty("sourceApiName") val sourceApiName: String?,
|
|
|
|
@JsonProperty("poster") val poster: String?,
|
|
|
|
@JsonProperty("name") val name: String?,
|
|
|
|
@JsonProperty("season") val season: Int?,
|
2022-04-03 20:14:51 +00:00
|
|
|
@JsonProperty("episode") val episode: Int?,
|
|
|
|
@JsonProperty("type") val type: TvType?,
|
2021-07-03 20:59:46 +00:00
|
|
|
)
|
|
|
|
|
2021-07-04 17:00:04 +00:00
|
|
|
data class DownloadItem(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("source") val source: String?,
|
|
|
|
@JsonProperty("folder") val folder: String?,
|
|
|
|
@JsonProperty("ep") val ep: DownloadEpisodeMetadata,
|
|
|
|
@JsonProperty("links") val links: List<ExtractorLink>,
|
2021-07-04 17:00:04 +00:00
|
|
|
)
|
|
|
|
|
2021-07-05 18:09:37 +00:00
|
|
|
data class DownloadResumePackage(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("item") val item: DownloadItem,
|
|
|
|
@JsonProperty("linkIndex") val linkIndex: Int?,
|
2021-07-05 18:09:37 +00:00
|
|
|
)
|
|
|
|
|
2021-07-05 20:28:50 +00:00
|
|
|
data class DownloadedFileInfo(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("totalBytes") val totalBytes: Long,
|
|
|
|
@JsonProperty("relativePath") val relativePath: String,
|
|
|
|
@JsonProperty("displayName") val displayName: String,
|
|
|
|
@JsonProperty("extraInfo") val extraInfo: String? = null,
|
|
|
|
@JsonProperty("basePath") val basePath: String? = null // null is for legacy downloads. See getDefaultPath()
|
2021-07-05 20:28:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
data class DownloadedFileInfoResult(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("fileLength") val fileLength: Long,
|
|
|
|
@JsonProperty("totalBytes") val totalBytes: Long,
|
|
|
|
@JsonProperty("path") val path: Uri,
|
2021-07-05 20:28:50 +00:00
|
|
|
)
|
|
|
|
|
2021-07-08 17:46:47 +00:00
|
|
|
data class DownloadQueueResumePackage(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("index") val index: Int,
|
|
|
|
@JsonProperty("pkg") val pkg: DownloadResumePackage,
|
2021-07-08 17:46:47 +00:00
|
|
|
)
|
|
|
|
|
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
|
2021-12-24 16:09:01 +00:00
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
// will not download the next one, but is still classified as an error
|
|
|
|
private const val ERROR_DELETING_FILE = 3
|
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-12-24 16:09:01 +00:00
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
//private const val ERROR_OPEN_FILE = -3
|
2021-07-04 17:00:04 +00:00
|
|
|
private const val ERROR_TOO_SMALL_CONNECTION = -4
|
2021-12-24 16:09:01 +00:00
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
//private const val ERROR_WRONG_CONTENT = -5
|
2021-07-04 17:00:04 +00:00
|
|
|
private const val ERROR_CONNECTION_ERROR = -6
|
2021-12-24 16:09:01 +00:00
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
//private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7
|
|
|
|
//private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8
|
2021-07-05 18:09:37 +00:00
|
|
|
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
|
|
|
|
2021-10-07 17:40:31 +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
|
2021-08-22 17:14:48 +00:00
|
|
|
} catch (e: Exception) {
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2021-08-19 20:05:18 +00:00
|
|
|
return null
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-25 18:04:40 +00:00
|
|
|
/**
|
|
|
|
* @param hlsProgress will together with hlsTotal display another notification if used, to lessen the confusion about estimated size.
|
|
|
|
* */
|
2021-10-14 16:13:34 +00:00
|
|
|
private 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,
|
2021-12-25 18:04:40 +00:00
|
|
|
notificationCallback: (Int, Notification) -> Unit,
|
|
|
|
hlsProgress: Long? = null,
|
|
|
|
hlsTotal: Long? = null,
|
2021-09-14 22:18:03 +00:00
|
|
|
|
2021-12-25 18:04:40 +00:00
|
|
|
): Notification? {
|
2021-12-24 16:09:01 +00:00
|
|
|
try {
|
|
|
|
if (total <= 0) return null// crash, invalid data
|
2021-09-14 22:18:03 +00:00
|
|
|
|
|
|
|
// main { // DON'T WANT TO SLOW IT DOWN
|
2021-12-24 16:09:01 +00:00
|
|
|
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-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (ep.sourceApiName != null) {
|
|
|
|
builder.setSubText(ep.sourceApiName)
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2021-12-24 16:09:01 +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-12-25 18:04:40 +00:00
|
|
|
val pendingIntent: PendingIntent =
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
|
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
|
|
|
} else {
|
|
|
|
PendingIntent.getActivity(context, 0, intent, 0)
|
|
|
|
}
|
2021-12-24 16:09:01 +00:00
|
|
|
builder.setContentIntent(pendingIntent)
|
2021-10-14 16:13:34 +00:00
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +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
|
|
|
|
2021-12-24 16:09:01 +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
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
if (ep.poster != null) {
|
|
|
|
val poster = withContext(Dispatchers.IO) {
|
|
|
|
context.getImageBitmapFromUrl(ep.poster)
|
|
|
|
}
|
|
|
|
if (poster != null)
|
|
|
|
builder.setLargeIcon(poster)
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2021-12-25 18:04:40 +00:00
|
|
|
val progressPercentage: Long
|
|
|
|
val progressMbString: String
|
|
|
|
val totalMbString: String
|
|
|
|
val suffix: String
|
|
|
|
|
|
|
|
if (hlsProgress != null && hlsTotal != null) {
|
|
|
|
progressPercentage = hlsProgress.toLong() * 100 / hlsTotal
|
|
|
|
progressMbString = hlsProgress.toString()
|
|
|
|
totalMbString = hlsTotal.toString()
|
|
|
|
suffix = " - %.1f MB".format(progress / 1000000f)
|
|
|
|
} else {
|
|
|
|
progressPercentage = progress * 100 / total
|
|
|
|
progressMbString = "%.1f MB".format(progress / 1000000f)
|
|
|
|
totalMbString = "%.1f MB".format(total / 1000000f)
|
|
|
|
suffix = ""
|
|
|
|
}
|
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
val bigText =
|
|
|
|
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
|
2021-12-25 18:04:40 +00:00
|
|
|
(if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix"
|
2021-12-24 16:09:01 +00:00
|
|
|
} 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
val bodyStyle = NotificationCompat.BigTextStyle()
|
|
|
|
bodyStyle.bigText(bigText)
|
|
|
|
builder.setStyle(bodyStyle)
|
|
|
|
} else {
|
2021-12-25 18:04:40 +00:00
|
|
|
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
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
builder.setContentText(txt)
|
2021-07-03 20:59:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-24 16:09:01 +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
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (state == DownloadType.IsPaused) {
|
|
|
|
actionTypes.add(DownloadActionType.Resume)
|
|
|
|
actionTypes.add(DownloadActionType.Stop)
|
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +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
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
actionResultIntent.putExtra(
|
|
|
|
"type", when (i) {
|
|
|
|
DownloadActionType.Resume -> "resume"
|
|
|
|
DownloadActionType.Pause -> "pause"
|
|
|
|
DownloadActionType.Stop -> "stop"
|
|
|
|
}
|
|
|
|
)
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
actionResultIntent.putExtra("id", ep.id)
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +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 * 1000000 + ep.id),
|
|
|
|
actionResultIntent,
|
|
|
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
|
|
)
|
2021-09-14 22:18:03 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
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-12-24 16:09:01 +00:00
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (!hasCreatedNotChanel) {
|
|
|
|
context.createNotificationChannel()
|
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +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)
|
|
|
|
}
|
|
|
|
return notification
|
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
|
|
|
return null
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-04 17:00:04 +00:00
|
|
|
private const val reservedChars = "|\\?*<\":>+[]/\'"
|
2022-08-07 16:01:32 +00:00
|
|
|
fun sanitizeFilename(name: String, removeSpaces: Boolean= false): String {
|
2021-07-04 00:59:51 +00:00
|
|
|
var tempName = name
|
|
|
|
for (c in reservedChars) {
|
|
|
|
tempName = tempName.replace(c, ' ')
|
|
|
|
}
|
2022-08-07 16:01:32 +00:00
|
|
|
if (removeSpaces) tempName = tempName.replace(" ", "")
|
2021-07-04 17:00:04 +00:00
|
|
|
return tempName.replace(" ", " ").trim(' ')
|
|
|
|
}
|
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
@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))
|
2021-12-03 22:48:30 +00:00
|
|
|
val name =
|
|
|
|
c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
|
2021-08-22 17:14:48 +00:00
|
|
|
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) {
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2021-08-22 17:14:48 +00:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
/**
|
|
|
|
* Used for getting video player subs.
|
|
|
|
* @return List of pairs for the files in this format: <Name, Uri>
|
|
|
|
* */
|
2021-12-03 22:48:30 +00:00
|
|
|
fun getFolder(
|
|
|
|
context: Context,
|
|
|
|
relativePath: String,
|
|
|
|
basePath: String?
|
|
|
|
): List<Pair<String, Uri>>? {
|
2021-11-06 21:06:13 +00:00
|
|
|
val base = basePathToFile(context, basePath)
|
2021-11-01 15:33:46 +00:00
|
|
|
val folder = base?.gotoDir(relativePath, false)
|
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) {
|
2021-08-22 17:14:48 +00:00
|
|
|
return context.contentResolver?.getExistingFolderStartName(relativePath)
|
|
|
|
} else {
|
2021-11-01 15:33:46 +00:00
|
|
|
// val normalPath =
|
|
|
|
// "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace(
|
|
|
|
// '/',
|
|
|
|
// File.separatorChar
|
|
|
|
// )
|
|
|
|
// val folder = File(normalPath)
|
|
|
|
if (folder?.isDirectory == true) {
|
|
|
|
return folder.listFiles()?.map { Pair(it.name ?: "", it.uri) }
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
|
|
|
}
|
2021-11-01 15:33:46 +00:00
|
|
|
return null
|
|
|
|
// }
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-07-05 18:09:37 +00:00
|
|
|
@RequiresApi(Build.VERSION_CODES.Q)
|
2021-12-03 22:48:30 +00:00
|
|
|
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) {
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2021-08-09 14:40:20 +00:00
|
|
|
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) {
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2021-08-09 14:40:20 +00:00
|
|
|
null
|
|
|
|
}
|
2021-07-05 20:28:50 +00:00
|
|
|
}
|
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
data class CreateNotificationMetadata(
|
|
|
|
val type: DownloadType,
|
|
|
|
val bytesDownloaded: Long,
|
|
|
|
val bytesTotal: Long,
|
2021-12-25 18:04:40 +00:00
|
|
|
val hlsProgress: Long? = null,
|
|
|
|
val hlsTotal: Long? = null,
|
2021-08-22 17:14:48 +00:00
|
|
|
)
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
/**
|
|
|
|
* Sets up the appropriate file and creates a data stream from the file.
|
|
|
|
* Used for initializing downloads.
|
|
|
|
* */
|
2022-04-10 22:00:03 +00:00
|
|
|
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 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-11-01 15:33:46 +00:00
|
|
|
val baseFile = context.getBasePath()
|
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.first?.isDownloadDir() == true) {
|
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 =
|
2021-12-03 22:48:30 +00:00
|
|
|
cr.getExistingDownloadUriOrNullQ(
|
|
|
|
folder ?: "",
|
|
|
|
displayName
|
|
|
|
) // CURRENT FILE WITH THE SAME PATH
|
2021-08-30 17:11:04 +00:00
|
|
|
|
|
|
|
fileLength =
|
2021-12-03 22:48:30 +00:00
|
|
|
if (currentExistingFile == null || !resume) 0 else (cr.getFileLength(
|
|
|
|
currentExistingFile
|
|
|
|
)
|
2021-08-30 17:11:04 +00:00
|
|
|
?: 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) {
|
2021-12-03 22:48:30 +00:00
|
|
|
|
|
|
|
// Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents
|
|
|
|
// downloading to /Downloads yet it works with null
|
|
|
|
|
|
|
|
"vtt" -> null // "text/vtt"
|
2021-08-30 17:11:04 +00:00
|
|
|
"mp4" -> "video/mp4"
|
2021-12-03 22:48:30 +00:00
|
|
|
"srt" -> null // "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)
|
2021-11-02 14:25:12 +00:00
|
|
|
put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
|
2021-08-30 17:11:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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-11-01 15:33:46 +00:00
|
|
|
val subDir = baseFile.first?.gotoDir(folder)
|
|
|
|
val rFile = subDir?.findFile(displayName)
|
|
|
|
if (rFile?.exists() != true) {
|
2021-08-30 17:11:04 +00:00
|
|
|
fileLength = 0
|
2021-11-04 15:11:28 +00:00
|
|
|
if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE)
|
2021-08-30 17:11:04 +00:00
|
|
|
} else {
|
|
|
|
if (resume) {
|
2021-11-02 14:25:12 +00:00
|
|
|
fileLength = rFile.size()
|
2021-08-30 17:11:04 +00:00
|
|
|
} else {
|
|
|
|
fileLength = 0
|
2021-11-04 15:11:28 +00:00
|
|
|
if (!rFile.delete()) return StreamData(ERROR_DELETING_FILE)
|
|
|
|
if (subDir.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE)
|
2021-08-30 17:11:04 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-03 22:48:30 +00:00
|
|
|
fileStream = (subDir.findFile(displayName)
|
|
|
|
?: subDir.createFile(displayName))!!.openOutputStream()
|
2021-11-01 15:33:46 +00:00
|
|
|
// fileStream = FileOutputStream(rFile, false)
|
|
|
|
if (fileLength == 0L) resume = false
|
2021-08-30 17:11:04 +00:00
|
|
|
}
|
2021-09-01 21:30:21 +00:00
|
|
|
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
|
|
|
|
}
|
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
fun downloadThing(
|
2021-07-03 20:59:46 +00:00
|
|
|
context: Context,
|
2021-08-22 17:14:48 +00:00
|
|
|
link: IDownloadableMinimum,
|
|
|
|
name: String,
|
2021-07-04 00:59:51 +00:00
|
|
|
folder: String?,
|
2021-08-22 17:14:48 +00:00
|
|
|
extension: String,
|
|
|
|
tryResume: Boolean,
|
|
|
|
parentId: Int?,
|
2022-02-25 23:04:00 +00:00
|
|
|
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-11-01 15:33:46 +00:00
|
|
|
val basePath = context.getBasePath()
|
|
|
|
|
2021-09-01 21:30:21 +00:00
|
|
|
val displayName = getDisplayName(name, extension)
|
2021-12-03 22:48:30 +00:00
|
|
|
val relativePath =
|
2021-12-25 18:04:40 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath(
|
|
|
|
folder
|
|
|
|
) else folder
|
2021-07-05 00:55:07 +00:00
|
|
|
|
2021-07-05 18:09:37 +00:00
|
|
|
fun deleteFile(): Int {
|
2021-11-02 14:25:12 +00:00
|
|
|
return delete(context, name, relativePath, extension, parentId, basePath.first)
|
2021-07-03 20:59:46 +00:00
|
|
|
}
|
2021-07-04 00:59:51 +00:00
|
|
|
|
2021-11-02 14:25:12 +00:00
|
|
|
val stream = setupStream(context, name, relativePath, extension, tryResume)
|
2021-09-01 21:30:21 +00:00
|
|
|
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
|
2021-09-14 22:18:03 +00:00
|
|
|
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")
|
2021-09-11 18:44:37 +00:00
|
|
|
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-12-03 22:48:30 +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
|
|
|
|
2021-08-22 17:14:48 +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
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
parentId?.let {
|
2021-12-16 23:45:20 +00:00
|
|
|
setKey(
|
2021-09-14 22:18:03 +00:00
|
|
|
KEY_DOWNLOAD_INFO,
|
|
|
|
it.toString(),
|
2021-11-02 14:25:12 +00:00
|
|
|
DownloadedFileInfo(
|
|
|
|
bytesTotal,
|
|
|
|
relativePath ?: "",
|
|
|
|
displayName,
|
|
|
|
basePath = basePath.second
|
|
|
|
)
|
2021-09-14 22:18:03 +00:00
|
|
|
)
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
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
|
|
|
|
2021-08-22 17:14:48 +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
|
|
|
}
|
|
|
|
|
2021-12-03 22:48:30 +00:00
|
|
|
createNotificationCallback.invoke(
|
|
|
|
CreateNotificationMetadata(
|
|
|
|
type,
|
|
|
|
bytesDownloaded,
|
|
|
|
bytesTotal
|
|
|
|
)
|
|
|
|
)
|
2021-08-22 17:14:48 +00:00
|
|
|
/*createNotification(
|
2021-07-03 20:59:46 +00:00
|
|
|
context,
|
|
|
|
source,
|
|
|
|
link.name,
|
|
|
|
ep,
|
|
|
|
type,
|
|
|
|
bytesDownloaded,
|
|
|
|
bytesTotal
|
2021-08-22 17:14:48 +00:00
|
|
|
)*/
|
2021-07-03 20:59:46 +00:00
|
|
|
}
|
|
|
|
|
2021-07-28 01:04:32 +00:00
|
|
|
val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
|
2021-08-22 17:14:48 +00:00
|
|
|
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-12-16 23:45:20 +00:00
|
|
|
removeKey(KEY_RESUME_PACKAGES, event.first.toString())
|
|
|
|
saveQueue()
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
|
|
|
DownloadActionType.Resume -> {
|
|
|
|
isPaused = false; updateNotification()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
if (parentId != null)
|
|
|
|
downloadEvent += downloadEventListener
|
2021-07-28 01:04:32 +00:00
|
|
|
|
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) {
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2021-07-04 17:00:04 +00:00
|
|
|
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
|
|
|
|
2021-07-28 01:04:32 +00:00
|
|
|
try {
|
2021-08-22 17:14:48 +00:00
|
|
|
if (parentId != null)
|
|
|
|
downloadEvent -= downloadEventListener
|
2021-07-28 01:04:32 +00:00
|
|
|
} catch (e: Exception) {
|
2021-09-01 13:18:41 +00:00
|
|
|
logError(e)
|
2021-07-28 01:04:32 +00:00
|
|
|
}
|
|
|
|
|
2021-07-06 00:18:56 +00:00
|
|
|
try {
|
2021-08-22 17:14:48 +00:00
|
|
|
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 -> {
|
2021-08-22 17:14:48 +00:00
|
|
|
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
|
2021-07-04 17:00:04 +00:00
|
|
|
ERROR_CONNECTION_ERROR
|
|
|
|
}
|
|
|
|
isStopped -> {
|
2021-08-22 17:14:48 +00:00
|
|
|
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 -> {
|
2021-12-03 22:48:30 +00:00
|
|
|
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-11-02 14:25:12 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Guarantees a directory is present with the dir name (if createMissingDirectories is true).
|
|
|
|
* Works recursively when '/' is present.
|
|
|
|
* Will remove any file with the dir name if present and add directory.
|
2021-11-04 15:11:28 +00:00
|
|
|
* Will not work if the parent directory does not exist.
|
2021-11-02 14:25:12 +00:00
|
|
|
*
|
|
|
|
* @param directoryName if null will use the current path.
|
|
|
|
* @return UniFile / null if createMissingDirectories = false and folder is not found.
|
|
|
|
* */
|
2021-12-03 22:48:30 +00:00
|
|
|
private fun UniFile.gotoDir(
|
|
|
|
directoryName: String?,
|
|
|
|
createMissingDirectories: Boolean = true
|
|
|
|
): UniFile? {
|
2021-11-02 14:25:12 +00:00
|
|
|
|
|
|
|
// May give this error on scoped storage.
|
|
|
|
// W/DocumentsContract: Failed to create document
|
|
|
|
// java.lang.IllegalArgumentException: Parent document isn't a directory
|
|
|
|
|
|
|
|
// Not present in latest testing.
|
|
|
|
|
2021-11-02 14:27:05 +00:00
|
|
|
// println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}")
|
2021-11-02 14:25:12 +00:00
|
|
|
|
|
|
|
try {
|
2021-11-04 15:11:28 +00:00
|
|
|
// Creates itself from parent if doesn't exist.
|
|
|
|
if (!this.exists() && createMissingDirectories && !this.name.isNullOrBlank()) {
|
|
|
|
if (this.parentFile != null) {
|
|
|
|
this.parentFile?.createDirectory(this.name)
|
|
|
|
} else if (this.filePath != null) {
|
|
|
|
UniFile.fromFile(File(this.filePath!!).parentFile)?.createDirectory(this.name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-02 14:25:12 +00:00
|
|
|
val allDirectories = directoryName?.split("/")
|
|
|
|
return if (allDirectories?.size == 1 || allDirectories == null) {
|
|
|
|
val found = this.findFile(directoryName)
|
|
|
|
when {
|
|
|
|
directoryName.isNullOrBlank() -> this
|
|
|
|
found?.isDirectory == true -> found
|
|
|
|
|
|
|
|
!createMissingDirectories -> null
|
|
|
|
// Below creates directories
|
|
|
|
found?.isFile == true -> {
|
|
|
|
found.delete()
|
|
|
|
this.createDirectory(directoryName)
|
|
|
|
}
|
|
|
|
this.isDirectory -> this.createDirectory(directoryName)
|
|
|
|
else -> this.parentFile?.createDirectory(directoryName)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var currentDirectory = this
|
|
|
|
allDirectories.forEach {
|
|
|
|
// If the next directory is not found it returns the deepest directory possible.
|
|
|
|
val nextDir = currentDirectory.gotoDir(it, createMissingDirectories)
|
|
|
|
currentDirectory = nextDir ?: return null
|
|
|
|
}
|
|
|
|
currentDirectory
|
|
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
private fun getDisplayName(name: String, extension: String): String {
|
|
|
|
return "$name.$extension"
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the default download path as an UniFile.
|
|
|
|
* Vital for legacy downloads, be careful about changing anything here.
|
|
|
|
*
|
|
|
|
* As of writing UniFile is used for everything but download directory on scoped storage.
|
|
|
|
* Special ContentResolver fuckery is needed for that as UniFile doesn't work.
|
|
|
|
* */
|
|
|
|
fun getDownloadDir(): UniFile? {
|
|
|
|
// See https://www.py4u.net/discuss/614761
|
|
|
|
return UniFile.fromFile(
|
|
|
|
File(
|
|
|
|
Environment.getExternalStorageDirectory().absolutePath + File.separatorChar +
|
|
|
|
Environment.DIRECTORY_DOWNLOADS
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-11-02 14:25:12 +00:00
|
|
|
@Deprecated("TODO fix UniFile to work with download directory.")
|
2021-09-01 21:30:21 +00:00
|
|
|
private fun getRelativePath(folder: String?): String {
|
2021-12-03 22:48:30 +00:00
|
|
|
return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace(
|
|
|
|
'/',
|
|
|
|
File.separatorChar
|
|
|
|
)
|
2021-09-01 21:30:21 +00:00
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
/**
|
|
|
|
* Turns a string to an UniFile. Used for stored string paths such as settings.
|
|
|
|
* Should only be used to get a download path.
|
|
|
|
* */
|
|
|
|
private fun basePathToFile(context: Context, path: String?): UniFile? {
|
|
|
|
return when {
|
2021-11-02 14:25:12 +00:00
|
|
|
path.isNullOrBlank() -> getDownloadDir()
|
2021-11-01 15:33:46 +00:00
|
|
|
path.startsWith("content://") -> UniFile.fromUri(context, path.toUri())
|
|
|
|
else -> UniFile.fromFile(File(path))
|
|
|
|
}
|
2021-09-01 21:30:21 +00:00
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
/**
|
|
|
|
* Base path where downloaded things should be stored, changes depending on settings.
|
|
|
|
* Returns the file and a string to be stored for future file retrieval.
|
|
|
|
* UniFile.filePath is not sufficient for storage.
|
|
|
|
* */
|
|
|
|
fun Context.getBasePath(): Pair<UniFile?, String?> {
|
|
|
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
|
|
|
val basePathSetting = settingsManager.getString(getString(R.string.download_path_key), null)
|
|
|
|
return basePathToFile(this, basePathSetting) to basePathSetting
|
|
|
|
}
|
|
|
|
|
2022-03-04 15:39:56 +00:00
|
|
|
fun UniFile?.isDownloadDir(): Boolean {
|
2021-11-01 15:33:46 +00:00
|
|
|
return this != null && this.filePath == getDownloadDir()?.filePath
|
2021-09-01 21:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun delete(
|
|
|
|
context: Context,
|
|
|
|
name: String,
|
|
|
|
folder: String?,
|
|
|
|
extension: String,
|
|
|
|
parentId: Int?,
|
2021-11-01 15:33:46 +00:00
|
|
|
basePath: UniFile?
|
2021-09-01 21:30:21 +00:00
|
|
|
): Int {
|
|
|
|
val displayName = getDisplayName(name, extension)
|
|
|
|
|
2022-02-07 00:05:21 +00:00
|
|
|
// delete all subtitle files
|
|
|
|
if (extension == "mp4") {
|
|
|
|
try {
|
|
|
|
delete(context, name, folder, "vtt", parentId, basePath)
|
|
|
|
delete(context, name, folder, "srt", parentId, basePath)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
// If scoped storage and using download dir (not accessible with UniFile)
|
2021-12-16 23:45:20 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) {
|
2021-11-01 15:33:46 +00:00
|
|
|
val relativePath = getRelativePath(folder)
|
2021-12-03 22:48:30 +00:00
|
|
|
val lastContent =
|
|
|
|
context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
|
2021-09-01 21:30:21 +00:00
|
|
|
if (lastContent != null) {
|
|
|
|
context.contentResolver.delete(lastContent, null, null)
|
|
|
|
}
|
|
|
|
} else {
|
2021-11-01 15:33:46 +00:00
|
|
|
val dir = basePath?.gotoDir(folder)
|
2021-11-02 14:25:12 +00:00
|
|
|
val file = dir?.findFile(displayName)
|
|
|
|
val success = file?.delete()
|
2021-11-01 15:33:46 +00:00
|
|
|
if (success != true) return ERROR_DELETING_FILE else {
|
|
|
|
// Cleans up empty directory
|
|
|
|
if (dir.listFiles()?.isEmpty() == true) dir.delete()
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
parentId?.let {
|
|
|
|
downloadDeleteEvent.invoke(parentId)
|
|
|
|
}
|
2021-09-01 21:30:21 +00:00
|
|
|
}
|
|
|
|
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-11-02 14:25:12 +00:00
|
|
|
createNotificationCallback: (CreateNotificationMetadata) -> Unit
|
2021-09-01 12:21:03 +00:00
|
|
|
): 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(
|
2022-05-06 11:55:56 +00:00
|
|
|
link.url, link.quality, mapOf("referer" to link.referer)
|
2021-09-01 21:30:21 +00:00
|
|
|
)
|
2021-09-01 12:21:03 +00:00
|
|
|
|
2021-09-01 21:30:21 +00:00
|
|
|
var realIndex = startIndex ?: 0
|
2021-11-02 14:25:12 +00:00
|
|
|
val basePath = context.getBasePath()
|
|
|
|
|
2021-12-03 22:48:30 +00:00
|
|
|
val relativePath =
|
2021-12-25 18:04:40 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath(
|
|
|
|
folder
|
|
|
|
) else folder
|
2021-11-02 14:25:12 +00:00
|
|
|
|
|
|
|
val stream = setupStream(context, name, relativePath, extension, realIndex > 0)
|
2021-09-01 21:30:21 +00:00
|
|
|
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!!
|
2022-07-30 15:43:37 +00:00
|
|
|
val tsIterator = runBlocking {
|
|
|
|
m3u8Helper.hlsYield(listOf(m3u8), realIndex)
|
|
|
|
}
|
2021-09-01 12:21:03 +00:00
|
|
|
|
2021-09-01 21:30:21 +00:00
|
|
|
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 {
|
2021-11-02 14:25:12 +00:00
|
|
|
return delete(context, name, relativePath, extension, parentId, basePath.first)
|
2021-09-01 21:30:21 +00:00
|
|
|
}
|
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 {
|
2021-12-16 23:45:20 +00:00
|
|
|
setKey(
|
2021-09-01 21:30:21 +00:00
|
|
|
KEY_DOWNLOAD_INFO,
|
|
|
|
it.toString(),
|
|
|
|
DownloadedFileInfo(
|
2021-10-16 20:47:35 +00:00
|
|
|
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
|
2021-11-02 14:25:12 +00:00
|
|
|
relativePath ?: "",
|
2021-09-01 21:30:21 +00:00
|
|
|
displayName,
|
2021-11-01 15:33:46 +00:00
|
|
|
tsProgress.toString(),
|
|
|
|
basePath = basePath.second
|
2021-09-01 21:30:21 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2021-09-01 12:21:03 +00:00
|
|
|
}
|
2021-11-01 15:33:46 +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))
|
2021-09-14 22:18:03 +00:00
|
|
|
downloadProgressEvent.invoke(
|
|
|
|
Triple(
|
|
|
|
id,
|
|
|
|
bytesDownloaded,
|
2021-12-07 18:17:34 +00:00
|
|
|
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
|
2021-09-14 22:18:03 +00:00
|
|
|
)
|
|
|
|
)
|
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,
|
2021-12-07 18:17:34 +00:00
|
|
|
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
|
2021-12-25 18:04:40 +00:00
|
|
|
tsProgress,
|
|
|
|
totalTs
|
2021-09-01 21:30:21 +00:00
|
|
|
)
|
|
|
|
)
|
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
|
|
|
|
2022-04-03 20:14:51 +00:00
|
|
|
fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String {
|
|
|
|
return getFileName(context, metadata.name, metadata.episode, metadata.season)
|
|
|
|
}
|
|
|
|
|
2022-04-06 15:16:08 +00:00
|
|
|
private fun getFileName(
|
|
|
|
context: Context,
|
|
|
|
epName: String?,
|
|
|
|
episode: Int?,
|
|
|
|
season: Int?
|
|
|
|
): String {
|
2022-04-03 20:14:51 +00:00
|
|
|
// kinda ugly ik
|
|
|
|
return sanitizeFilename(
|
|
|
|
if (epName == null) {
|
|
|
|
if (season != null) {
|
|
|
|
"${context.getString(R.string.season)} $season ${context.getString(R.string.episode)} $episode"
|
|
|
|
} else {
|
|
|
|
"${context.getString(R.string.episode)} $episode"
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (episode != null) {
|
|
|
|
if (season != null) {
|
|
|
|
"${context.getString(R.string.season)} $season ${context.getString(R.string.episode)} $episode - $epName"
|
|
|
|
} else {
|
|
|
|
"${context.getString(R.string.episode)} $episode - $epName"
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
epName
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
private fun downloadSingleEpisode(
|
|
|
|
context: Context,
|
|
|
|
source: String?,
|
|
|
|
folder: String?,
|
|
|
|
ep: DownloadEpisodeMetadata,
|
|
|
|
link: ExtractorLink,
|
2021-09-14 22:18:03 +00:00
|
|
|
notificationCallback: (Int, Notification) -> Unit,
|
2021-08-22 17:14:48 +00:00
|
|
|
tryResume: Boolean = false,
|
|
|
|
): Int {
|
2022-04-03 20:14:51 +00:00
|
|
|
val name = getFileName(context, ep)
|
2021-08-22 17:14:48 +00:00
|
|
|
|
2022-02-25 23:04:00 +00:00
|
|
|
// Make sure this is cancelled when download is done or cancelled.
|
|
|
|
val extractorJob = ioSafe {
|
|
|
|
if (link.extractorData != null) {
|
|
|
|
getApiFromNameNull(link.source)?.extractorVerifierJob(link.extractorData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-03 20:23:06 +00:00
|
|
|
if (link.isM3u8 || URI(link.url).path.endsWith(".m3u8")) {
|
2021-09-01 21:30:21 +00:00
|
|
|
val startIndex = if (tryResume) {
|
2021-09-14 22:18:03 +00:00
|
|
|
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 ->
|
2021-09-14 22:18:03 +00:00
|
|
|
main {
|
|
|
|
createNotification(
|
|
|
|
context,
|
|
|
|
source,
|
|
|
|
link.name,
|
|
|
|
ep,
|
|
|
|
meta.type,
|
|
|
|
meta.bytesDownloaded,
|
|
|
|
meta.bytesTotal,
|
2021-12-25 18:04:40 +00:00
|
|
|
notificationCallback,
|
|
|
|
meta.hlsProgress,
|
|
|
|
meta.hlsTotal
|
2021-09-14 22:18:03 +00:00
|
|
|
)
|
|
|
|
}
|
2022-02-25 23:04:00 +00:00
|
|
|
}.also { extractorJob.cancel() }
|
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 ->
|
2021-09-14 22:18:03 +00:00
|
|
|
main {
|
|
|
|
createNotification(
|
|
|
|
context,
|
|
|
|
source,
|
|
|
|
link.name,
|
|
|
|
ep,
|
|
|
|
meta.type,
|
|
|
|
meta.bytesDownloaded,
|
|
|
|
meta.bytesTotal,
|
|
|
|
notificationCallback
|
|
|
|
)
|
|
|
|
}
|
2021-09-01 12:02:32 +00:00
|
|
|
}
|
2022-02-25 23:04:00 +00:00
|
|
|
}.also { extractorJob.cancel() } ?: ERROR_UNKNOWN
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-09-14 22:18:03 +00:00
|
|
|
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))
|
2021-09-14 22:18:03 +00:00
|
|
|
/** 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-12-16 23:45:20 +00:00
|
|
|
setKey(
|
2021-12-03 22:48:30 +00:00
|
|
|
KEY_RESUME_PACKAGES,
|
|
|
|
id.toString(),
|
|
|
|
DownloadResumePackage(item, index)
|
|
|
|
)
|
2021-07-04 17:00:04 +00:00
|
|
|
val connectionResult = withContext(Dispatchers.IO) {
|
|
|
|
normalSafeApiCall {
|
2021-09-14 22:18:03 +00:00
|
|
|
downloadSingleEpisode(
|
|
|
|
context,
|
|
|
|
item.source,
|
|
|
|
item.folder,
|
|
|
|
item.ep,
|
|
|
|
link,
|
|
|
|
notificationCallback,
|
|
|
|
resume
|
2021-11-04 15:11:28 +00:00
|
|
|
).also { println("Single episode finished with return code: $it") }
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (connectionResult != null && connectionResult > 0) { // SUCCESS
|
2021-12-16 23:45:20 +00:00
|
|
|
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)
|
2021-09-14 22:18:03 +00:00
|
|
|
// Because otherwise notifications will not get caught by the workmanager
|
|
|
|
downloadCheckUsingWorker(context)
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-09-14 22:18:03 +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? {
|
2022-04-06 15:16:08 +00:00
|
|
|
try {
|
|
|
|
val info =
|
|
|
|
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null
|
|
|
|
val base = basePathToFile(context, info.basePath)
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) {
|
|
|
|
val cr = context.contentResolver ?: return null
|
|
|
|
val fileUri =
|
|
|
|
cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName)
|
|
|
|
?: return null
|
|
|
|
val fileLength = cr.getFileLength(fileUri) ?: return null
|
|
|
|
if (fileLength == 0L) return null
|
|
|
|
return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri)
|
|
|
|
} else {
|
2021-11-04 16:07:29 +00:00
|
|
|
|
2022-04-06 15:16:08 +00:00
|
|
|
val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName)
|
2021-11-04 16:07:29 +00:00
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
|
|
|
|
// val dFile = File(normalPath)
|
2021-11-02 14:25:12 +00:00
|
|
|
|
2022-04-06 15:16:08 +00:00
|
|
|
if (file?.exists() != true) return null
|
2021-11-01 15:33:46 +00:00
|
|
|
|
2022-04-06 15:16:08 +00:00
|
|
|
return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri)
|
|
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
|
|
|
return null
|
2021-11-02 14:25:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the true download size as Scoped Storage sometimes wrongly returns 0.
|
|
|
|
* */
|
|
|
|
fun UniFile.size(): Long {
|
|
|
|
val len = length()
|
|
|
|
return if (len <= 1) {
|
|
|
|
val inputStream = this.openInputStream()
|
|
|
|
return inputStream.available().toLong().also { inputStream.closeQuietly() }
|
|
|
|
} else {
|
|
|
|
len
|
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-12-03 22:48:30 +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-11-01 15:33:46 +00:00
|
|
|
val base = basePathToFile(context, info.basePath)
|
2021-12-16 23:45:20 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) {
|
2021-07-05 20:28:50 +00:00
|
|
|
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-11-01 15:33:46 +00:00
|
|
|
val file = base?.gotoDir(info.relativePath)?.findFile(info.displayName)
|
|
|
|
// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName)
|
|
|
|
// val dFile = File(normalPath)
|
|
|
|
if (file?.exists() != true) return true
|
2021-11-02 14:25:12 +00:00
|
|
|
return try {
|
|
|
|
file.delete()
|
|
|
|
} catch (e: Exception) {
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2021-11-02 14:25:12 +00:00
|
|
|
val cr = context.contentResolver
|
|
|
|
cr.delete(file.uri, null, null) > 0
|
|
|
|
}
|
2021-07-05 20:28:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? {
|
|
|
|
return context.getKey(KEY_RESUME_PACKAGES, id.toString())
|
|
|
|
}
|
|
|
|
|
2021-09-14 22:18:03 +00:00
|
|
|
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 }) {
|
2021-11-01 15:33:46 +00:00
|
|
|
// 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)
|
2021-09-14 22:18:03 +00:00
|
|
|
downloadCheck(context, notificationCallback)
|
2021-12-16 23:45:20 +00:00
|
|
|
if (setKey) saveQueue()
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
private fun saveQueue() {
|
2022-07-20 23:48:40 +00:00
|
|
|
try {
|
|
|
|
val dQueue =
|
|
|
|
downloadQueue.toList()
|
|
|
|
.mapIndexed { index, any -> DownloadQueueResumePackage(index, any) }
|
|
|
|
.toTypedArray()
|
|
|
|
setKey(KEY_RESUME_QUEUE_PACKAGES, dQueue)
|
|
|
|
} catch (e : Exception) {
|
|
|
|
logError(e)
|
|
|
|
}
|
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(
|
2021-09-14 22:18:03 +00:00
|
|
|
context: Context?,
|
2021-07-06 00:18:56 +00:00
|
|
|
source: String?,
|
2021-07-04 00:59:51 +00:00
|
|
|
folder: String?,
|
|
|
|
ep: DownloadEpisodeMetadata,
|
2021-09-14 22:18:03 +00:00
|
|
|
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()) {
|
2021-09-14 22:18:03 +00:00
|
|
|
downloadFromResume(
|
|
|
|
context,
|
|
|
|
DownloadResumePackage(DownloadItem(source, folder, ep, links), null),
|
|
|
|
notificationCallback
|
|
|
|
)
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-14 22:18:03 +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()
|
2021-12-16 23:45:20 +00:00
|
|
|
setKey(WORK_KEY_PACKAGE, key, pkg)
|
2021-09-14 22:18:03 +00:00
|
|
|
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()
|
2021-12-16 23:45:20 +00:00
|
|
|
setKey(WORK_KEY_INFO, key, info)
|
2021-09-14 22:18:03 +00:00
|
|
|
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
|
|
|
}
|