download not working

This commit is contained in:
LagradOst 2021-07-03 22:59:46 +02:00
parent 9da1ce6032
commit 602cd065ce
5 changed files with 168 additions and 210 deletions

View file

@ -71,8 +71,11 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// Exoplayer // Exoplayer
implementation 'com.google.android.exoplayer:exoplayer:2.14.0' implementation 'com.google.android.exoplayer:exoplayer:2.14.1'
implementation 'com.google.android.exoplayer:extension-cast:2.14.0' implementation 'com.google.android.exoplayer:extension-cast:2.14.1'
implementation "com.google.android.exoplayer:extension-mediasession:2.14.0" implementation "com.google.android.exoplayer:extension-mediasession:2.14.1"
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0" //implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
//download manager
implementation "com.anggrayudi:storage:0.9.0"
} }

View file

@ -147,11 +147,11 @@ class EpisodeAdapter(
clickCallback.invoke(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card))
} else { } else {
// clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) // 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 { } else {
// clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) // 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))
} }
} }
} }

View file

@ -341,10 +341,19 @@ class ResultFragment : Fragment() {
if (tempUrl != null) { if (tempUrl != null) {
viewModel.loadEpisode(episodeClick.data, true) { data -> viewModel.loadEpisode(episodeClick.data, true) { data ->
if (data is Resource.Success) { 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( VideoDownloadManager.DownloadEpisode(
requireContext(), requireContext(),
tempUrl, tempUrl,
episodeClick.data, meta,
data.value.links data.value.links
) )
} }

View file

@ -6,31 +6,23 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri 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.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
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.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.USER_AGENT import java.io.BufferedInputStream
import com.lagradost.cloudstream3.ui.result.ResultEpisode import java.io.InputStream
import java.io.File import java.net.URL
import java.util.concurrent.Executor import java.net.URLConnection
const val CHANNEL_ID = "cloudstream3.general" const val CHANNEL_ID = "cloudstream3.general"
@ -38,6 +30,11 @@ const val CHANNEL_NAME = "Downloads"
const val CHANNEL_DESCRIPT = "The download notification channel" const val CHANNEL_DESCRIPT = "The download notification channel"
object VideoDownloadManager { 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 @DrawableRes
const val imgDone = R.drawable.rddone const val imgDone = R.drawable.rddone
@ -76,6 +73,16 @@ object VideoDownloadManager {
Stop, 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 var hasCreatedNotChanel = false
private fun Context.createNotificationChannel() { private fun Context.createNotificationChannel() {
hasCreatedNotChanel = true hasCreatedNotChanel = true
@ -111,29 +118,22 @@ object VideoDownloadManager {
return null return null
} }
fun createNotification( private fun createNotification(
context: Context, context: Context,
text: String, source: String?,
source: String, linkName: String?,
ep: ResultEpisode, ep: DownloadEpisodeMetadata,
state: DownloadType, state: DownloadType,
progress: Long, progress: Long,
total: 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) val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setAutoCancel(true) .setAutoCancel(true)
.setColorized(true) .setColorized(true)
.setAutoCancel(true)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary)) .setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentText(text) .setContentTitle(ep.mainName)
.setSmallIcon( .setSmallIcon(
when (state) { when (state) {
DownloadType.IsDone -> imgDone DownloadType.IsDone -> imgDone
@ -143,19 +143,72 @@ object VideoDownloadManager {
DownloadType.IsStopped -> imgStopped 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) { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
builder.setProgress(total.toInt(), progress.toInt(), false) 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (ep.poster != null) { if (ep.poster != null) {
val poster = context.getImageBitmapFromUrl(ep.poster) val poster = context.getImageBitmapFromUrl(ep.poster)
if (poster != null) if (poster != null)
builder.setLargeIcon(poster) 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) { if ((state == DownloadType.IsDownloading || state == DownloadType.IsPaused) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val actionTypes: MutableList<DownloadActionType> = ArrayList() val actionTypes: MutableList<DownloadActionType> = ArrayList()
// INIT // INIT
@ -171,9 +224,9 @@ object VideoDownloadManager {
// ADD ACTIONS // ADD ACTIONS
for ((index, i) in actionTypes.withIndex()) { 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) { "type", when (i) {
DownloadActionType.Resume -> "resume" DownloadActionType.Resume -> "resume"
DownloadActionType.Pause -> "pause" DownloadActionType.Pause -> "pause"
@ -181,11 +234,11 @@ object VideoDownloadManager {
} }
) )
_resultIntent.putExtra("id", ep.id) actionResultIntent.putExtra("id", ep.id)
val pending: PendingIntent = PendingIntent.getService( val pending: PendingIntent = PendingIntent.getService(
context, 4337 + index + ep.id, context, 4337 + index + ep.id,
_resultIntent, actionResultIntent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
@ -215,96 +268,86 @@ object VideoDownloadManager {
} }
} }
//https://exoplayer.dev/downloading-media.html fun DownloadSingleEpisode(
fun DownloadSingleEpisode(context: Context, source: String, ep: ResultEpisode, link: ExtractorLink) { context: Context,
val url = link.url source: String?,
val headers = mapOf("User-Agent" to USER_AGENT, "Referer" to link.referer) 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 resume = false
val databaseProvider = ExoDatabaseProvider(context)
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()) {
// A download cache should not evict media, so should use a NoopCacheEvictor. return false
val downloadCache = SimpleCache(
downloadDirectory,
NoOpCacheEvictor(),
databaseProvider)
// Create a factory for reading the data from the network.
val dataSourceFactory = DefaultHttpDataSourceFactory()
// 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() }
// Create the download manager.
val downloadManager = DownloadManager(
context,
databaseProvider,
downloadCache,
dataSourceFactory,
downloadExecutor)
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())
val downloadRequest: DownloadRequest = builder.build()
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
)
} }
)*/ }
if (!dFile.exists()) {
dFile.createFile("video/mp4", name)
} }
public fun DownloadEpisode(context: Context, source: String, ep: ResultEpisode, links: List<ExtractorLink>) { // OPEN FILE
val fileStream = dFile.openOutputStream(context, resume) ?: return false
// CONNECT
val connection: URLConnection = URL(link.url).openConnection()
// 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
// READ DATA FROM CONNECTION
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
val buffer = ByteArray(1024)
var count: Int
var bytesDownloaded = resumeLength
fun updateNotification(type : DownloadType) {
createNotification(
context,
source,
link.name,
ep,
type,
bytesDownloaded,
bytesTotal
)
}
while (true) {
count = connectionInputStream.read(buffer)
if (count < 0) break
bytesDownloaded += count
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: DownloadEpisodeMetadata, links: List<ExtractorLink>) {
val validLinks = links.filter { !it.isM3u8 } val validLinks = links.filter { !it.isM3u8 }
if (validLinks.isNotEmpty()) { if (validLinks.isNotEmpty()) {
DownloadSingleEpisode(context, source, ep, validLinks.first()) DownloadSingleEpisode(context, source, ep, validLinks.first())
} }
} }
} }

View file

@ -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()
}
}