forked from recloudstream/cloudstream
download not working
This commit is contained in:
parent
9da1ce6032
commit
602cd065ce
5 changed files with 168 additions and 210 deletions
|
@ -71,8 +71,11 @@ dependencies {
|
|||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
|
||||
// Exoplayer
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.14.0'
|
||||
implementation 'com.google.android.exoplayer:extension-cast:2.14.0'
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:2.14.0"
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.14.1'
|
||||
implementation 'com.google.android.exoplayer:extension-cast:2.14.1'
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:2.14.1"
|
||||
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
|
||||
|
||||
//download manager
|
||||
implementation "com.anggrayudi:storage:0.9.0"
|
||||
}
|
|
@ -147,11 +147,11 @@ class EpisodeAdapter(
|
|||
clickCallback.invoke(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card))
|
||||
} else {
|
||||
// clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card))
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card))
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
|
||||
}
|
||||
} else {
|
||||
// clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card))
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card))
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -341,10 +341,19 @@ class ResultFragment : Fragment() {
|
|||
if (tempUrl != null) {
|
||||
viewModel.loadEpisode(episodeClick.data, true) { data ->
|
||||
if (data is Resource.Success) {
|
||||
val meta = VideoDownloadManager.DownloadEpisodeMetadata(
|
||||
episodeClick.data.id,
|
||||
currentHeaderName ?: return@loadEpisode,
|
||||
apiName ?: return@loadEpisode,
|
||||
episodeClick.data.poster,
|
||||
episodeClick.data.name,
|
||||
episodeClick.data.season,
|
||||
episodeClick.data.episode
|
||||
)
|
||||
VideoDownloadManager.DownloadEpisode(
|
||||
requireContext(),
|
||||
tempUrl,
|
||||
episodeClick.data,
|
||||
meta,
|
||||
data.value.links
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,31 +6,23 @@ import android.app.PendingIntent
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.anggrayudi.storage.extension.closeStream
|
||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
||||
import com.google.android.exoplayer2.offline.DownloadManager
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest
|
||||
import com.google.android.exoplayer2.offline.DownloadService
|
||||
import com.google.android.exoplayer2.offline.DownloadService.sendAddDownload
|
||||
import com.google.android.exoplayer2.offline.StreamKey
|
||||
import com.google.android.exoplayer2.scheduler.Requirements
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executor
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
||||
|
||||
const val CHANNEL_ID = "cloudstream3.general"
|
||||
|
@ -38,6 +30,11 @@ const val CHANNEL_NAME = "Downloads"
|
|||
const val CHANNEL_DESCRIPT = "The download notification channel"
|
||||
|
||||
object VideoDownloadManager {
|
||||
var maxConcurrentDownloads = 3
|
||||
|
||||
private const val USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
|
||||
@DrawableRes
|
||||
const val imgDone = R.drawable.rddone
|
||||
|
||||
|
@ -76,6 +73,16 @@ object VideoDownloadManager {
|
|||
Stop,
|
||||
}
|
||||
|
||||
data class DownloadEpisodeMetadata(
|
||||
val id: Int,
|
||||
val mainName: String,
|
||||
val sourceApiName: String?,
|
||||
val poster: String?,
|
||||
val name: String?,
|
||||
val season: Int?,
|
||||
val episode: Int?
|
||||
)
|
||||
|
||||
private var hasCreatedNotChanel = false
|
||||
private fun Context.createNotificationChannel() {
|
||||
hasCreatedNotChanel = true
|
||||
|
@ -111,29 +118,22 @@ object VideoDownloadManager {
|
|||
return null
|
||||
}
|
||||
|
||||
fun createNotification(
|
||||
private fun createNotification(
|
||||
context: Context,
|
||||
text: String,
|
||||
source: String,
|
||||
ep: ResultEpisode,
|
||||
source: String?,
|
||||
linkName: String?,
|
||||
ep: DownloadEpisodeMetadata,
|
||||
state: DownloadType,
|
||||
progress: Long,
|
||||
total: Long,
|
||||
) {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
data = source.toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setAutoCancel(true)
|
||||
.setColorized(true)
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
||||
.setContentText(text)
|
||||
.setContentTitle(ep.mainName)
|
||||
.setSmallIcon(
|
||||
when (state) {
|
||||
DownloadType.IsDone -> imgDone
|
||||
|
@ -143,19 +143,72 @@ object VideoDownloadManager {
|
|||
DownloadType.IsStopped -> imgStopped
|
||||
}
|
||||
)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
||||
if (ep.sourceApiName != null) {
|
||||
builder.setSubText(ep.sourceApiName)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
|
||||
builder.setContentIntent(pendingIntent)
|
||||
}
|
||||
|
||||
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
|
||||
builder.setProgress(total.toInt(), progress.toInt(), false)
|
||||
}
|
||||
|
||||
val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else ""
|
||||
val rowTwo = if (ep.season != null && ep.episode != null) {
|
||||
"S${ep.season}:E${ep.episode}" + rowTwoExtra
|
||||
} else if (ep.episode != null) {
|
||||
"Episode ${ep.episode}" + rowTwoExtra
|
||||
} else {
|
||||
(ep.name ?: "") + ""
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (ep.poster != null) {
|
||||
val poster = context.getImageBitmapFromUrl(ep.poster)
|
||||
if (poster != null)
|
||||
builder.setLargeIcon(poster)
|
||||
}
|
||||
|
||||
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)"
|
||||
} else if (state == DownloadType.IsFailed) {
|
||||
"Download Failed - $rowTwo"
|
||||
} else if (state == DownloadType.IsDone) {
|
||||
"Download Done - $rowTwo"
|
||||
} else {
|
||||
"Download Stopped - $rowTwo"
|
||||
}
|
||||
|
||||
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) {
|
||||
"Download Failed - $rowTwo"
|
||||
} else if (state == DownloadType.IsDone) {
|
||||
"Download Done - $rowTwo"
|
||||
} else {
|
||||
"Download Stopped - $rowTwo"
|
||||
}
|
||||
|
||||
builder.setContentText(txt)
|
||||
}
|
||||
|
||||
if ((state == DownloadType.IsDownloading || state == DownloadType.IsPaused) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val actionTypes: MutableList<DownloadActionType> = ArrayList()
|
||||
// INIT
|
||||
|
@ -171,9 +224,9 @@ object VideoDownloadManager {
|
|||
|
||||
// ADD ACTIONS
|
||||
for ((index, i) in actionTypes.withIndex()) {
|
||||
val _resultIntent = Intent(context, DownloadService::class.java)
|
||||
val actionResultIntent = Intent(context, DownloadService::class.java)
|
||||
|
||||
_resultIntent.putExtra(
|
||||
actionResultIntent.putExtra(
|
||||
"type", when (i) {
|
||||
DownloadActionType.Resume -> "resume"
|
||||
DownloadActionType.Pause -> "pause"
|
||||
|
@ -181,11 +234,11 @@ object VideoDownloadManager {
|
|||
}
|
||||
)
|
||||
|
||||
_resultIntent.putExtra("id", ep.id)
|
||||
actionResultIntent.putExtra("id", ep.id)
|
||||
|
||||
val pending: PendingIntent = PendingIntent.getService(
|
||||
context, 4337 + index + ep.id,
|
||||
_resultIntent,
|
||||
actionResultIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
|
@ -215,96 +268,86 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
//https://exoplayer.dev/downloading-media.html
|
||||
fun DownloadSingleEpisode(context: Context, source: String, ep: ResultEpisode, link: ExtractorLink) {
|
||||
val url = link.url
|
||||
val headers = mapOf("User-Agent" to USER_AGENT, "Referer" to link.referer)
|
||||
fun DownloadSingleEpisode(
|
||||
context: Context,
|
||||
source: String?,
|
||||
ep: DownloadEpisodeMetadata,
|
||||
link: ExtractorLink
|
||||
): Boolean {
|
||||
val name = (ep.name ?: "Episode ${ep.episode}")
|
||||
val path = "Downloads/Anime/$name.mp4"
|
||||
val dFile = DocumentFileCompat.fromSimplePath(context, basePath = path) ?: return false
|
||||
|
||||
// Note: This should be a singleton in your app.
|
||||
val databaseProvider = ExoDatabaseProvider(context)
|
||||
val resume = false
|
||||
|
||||
val downloadDirectory = File(Environment.getExternalStorageDirectory().path + "/Download/" + (ep.name ?: "Episode ${ep.episode}")) // File(context.cacheDir, "video_${ep.id}")
|
||||
if (!resume && dFile.exists()) {
|
||||
if (!dFile.delete()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (!dFile.exists()) {
|
||||
dFile.createFile("video/mp4", name)
|
||||
}
|
||||
|
||||
// A download cache should not evict media, so should use a NoopCacheEvictor.
|
||||
val downloadCache = SimpleCache(
|
||||
downloadDirectory,
|
||||
NoOpCacheEvictor(),
|
||||
databaseProvider)
|
||||
// OPEN FILE
|
||||
val fileStream = dFile.openOutputStream(context, resume) ?: return false
|
||||
|
||||
// Create a factory for reading the data from the network.
|
||||
val dataSourceFactory = DefaultHttpDataSourceFactory()
|
||||
// CONNECT
|
||||
val connection: URLConnection = URL(link.url).openConnection()
|
||||
|
||||
// Choose an executor for downloading data. Using Runnable::run will cause each download task to
|
||||
// download data on its own thread. Passing an executor that uses multiple threads will speed up
|
||||
// download tasks that can be split into smaller parts for parallel execution. Applications that
|
||||
// already have an executor for background downloads may wish to reuse their existing executor.
|
||||
val downloadExecutor = Executor { obj: Runnable -> obj.run() }
|
||||
// SET CONNECTION SETTINGS
|
||||
connection.connectTimeout = 10000
|
||||
connection.setRequestProperty("Accept-Encoding", "identity")
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT)
|
||||
if (link.referer.isNotEmpty()) connection.setRequestProperty("Referer", link.referer)
|
||||
if (resume) connection.setRequestProperty("Range", "bytes=${dFile.length()}-")
|
||||
val resumeLength = (if (resume) dFile.length() else 0)
|
||||
|
||||
// ON CONNECTION
|
||||
connection.connect()
|
||||
val contentLength = connection.contentLength
|
||||
if (contentLength < 5000000) return false // less than 5mb
|
||||
val bytesTotal = contentLength + resumeLength
|
||||
|
||||
// Create the download manager.
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
databaseProvider,
|
||||
downloadCache,
|
||||
dataSourceFactory,
|
||||
downloadExecutor)
|
||||
// READ DATA FROM CONNECTION
|
||||
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
|
||||
val buffer = ByteArray(1024)
|
||||
var count: Int
|
||||
var bytesDownloaded = resumeLength
|
||||
|
||||
val requirements = Requirements(Requirements.NETWORK)
|
||||
// Optionally, setters can be called to configure the download manager.
|
||||
downloadManager.requirements = requirements
|
||||
downloadManager.maxParallelDownloads = 3
|
||||
val builder = DownloadRequest.Builder(ep.id.toString(), link.url.toUri())
|
||||
fun updateNotification(type : DownloadType) {
|
||||
createNotification(
|
||||
context,
|
||||
source,
|
||||
link.name,
|
||||
ep,
|
||||
type,
|
||||
bytesDownloaded,
|
||||
bytesTotal
|
||||
)
|
||||
}
|
||||
|
||||
val downloadRequest: DownloadRequest = builder.build()
|
||||
while (true) {
|
||||
count = connectionInputStream.read(buffer)
|
||||
if (count < 0) break
|
||||
bytesDownloaded += count
|
||||
|
||||
DownloadService.sendAddDownload(
|
||||
context,
|
||||
VideoDownloadService::class.java,
|
||||
downloadRequest,
|
||||
/* foreground= */ true)
|
||||
/*
|
||||
val disposable = url.download(header = headers)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(
|
||||
onNext = { progress ->
|
||||
createNotification(
|
||||
context,
|
||||
"Downloading ${progress.downloadSizeStr()}/${progress.totalSizeStr()}",
|
||||
source,
|
||||
ep,
|
||||
DownloadType.IsDownloading,
|
||||
progress.downloadSize,
|
||||
progress.totalSize
|
||||
)
|
||||
},
|
||||
onComplete = {
|
||||
createNotification(
|
||||
context,
|
||||
"Download Done",
|
||||
source,
|
||||
ep,
|
||||
DownloadType.IsDone,
|
||||
0, 0
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
createNotification(
|
||||
context,
|
||||
"Download Failed",
|
||||
source,
|
||||
ep,
|
||||
DownloadType.IsFailed,
|
||||
0, 0
|
||||
)
|
||||
}
|
||||
)*/
|
||||
updateNotification(DownloadType.IsDownloading)
|
||||
fileStream.write(buffer, 0, count)
|
||||
}
|
||||
|
||||
// DOWNLOAD EXITED CORRECTLY
|
||||
updateNotification(DownloadType.IsDone)
|
||||
fileStream.closeStream()
|
||||
connectionInputStream.closeStream()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public fun DownloadEpisode(context: Context, source: String, ep: ResultEpisode, links: List<ExtractorLink>) {
|
||||
public fun DownloadEpisode(context: Context, source: String, ep: DownloadEpisodeMetadata, links: List<ExtractorLink>) {
|
||||
val validLinks = links.filter { !it.isM3u8 }
|
||||
if (validLinks.isNotEmpty()) {
|
||||
DownloadSingleEpisode(context, source, ep, validLinks.first())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.app.IntentService
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.google.android.exoplayer2.offline.DownloadManager
|
||||
import com.google.android.exoplayer2.offline.DownloadService
|
||||
import com.google.android.exoplayer2.scheduler.PlatformScheduler
|
||||
import com.google.android.exoplayer2.scheduler.Scheduler
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.UIHelper.colorFromAttribute
|
||||
import java.lang.Exception
|
||||
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
|
||||
class VideoDownloadService : DownloadService(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||
CHANNEL_ID,
|
||||
R.string.exo_download_notification_channel_name, /* channelDescriptionResourceId= */
|
||||
0) {
|
||||
override fun getDownloadManager(): DownloadManager {
|
||||
val ctx = this
|
||||
return ExoPlayerHelper.downloadManager.apply {
|
||||
requirements = DownloadManager.DEFAULT_REQUIREMENTS
|
||||
maxParallelDownloads = 3
|
||||
addListener(
|
||||
object : DownloadManager.Listener {
|
||||
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?,
|
||||
) {
|
||||
val intent = Intent(ctx, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(ctx, 0, intent, 0)
|
||||
val builder = NotificationCompat.Builder(ctx, CHANNEL_ID)
|
||||
.setAutoCancel(true)
|
||||
.setColorized(true)
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setColor(colorFromAttribute(R.attr.colorPrimary))
|
||||
.setContentText("${download.bytesDownloaded} / ${download.contentLength}")
|
||||
.setSmallIcon(
|
||||
VideoDownloadManager.imgDownloading
|
||||
)
|
||||
.setProgress((download.bytesDownloaded / 1000).toInt(),
|
||||
(download.contentLength / 1000).toInt(),
|
||||
false) // in case the size is over 2gb / 1000
|
||||
.setContentIntent(pendingIntent)
|
||||
builder.build()
|
||||
with(NotificationManagerCompat.from(ctx)) {
|
||||
// notificationId is a unique int for each notification that you must define
|
||||
notify(download.request.id.hashCode(), builder.build())
|
||||
}
|
||||
super.onDownloadChanged(downloadManager, download, finalException)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScheduler(): Scheduler =
|
||||
PlatformScheduler(this, JOB_ID)
|
||||
|
||||
override fun getForegroundNotification(downloads: MutableList<Download>): Notification {
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setAutoCancel(true)
|
||||
.setColorized(true)
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setColor(colorFromAttribute(R.attr.colorPrimary))
|
||||
.setContentText("Downloading ${downloads.size} item${if (downloads.size == 1) "" else "s"}")
|
||||
.setSmallIcon(
|
||||
VideoDownloadManager.imgDownloading
|
||||
)
|
||||
.setContentIntent(pendingIntent)
|
||||
return builder.build()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue