download not working

This commit is contained in:
LagradOst 2021-06-30 01:14:48 +02:00
parent 2c312cd9b4
commit 672dc8c8d1
15 changed files with 559 additions and 6 deletions

View file

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3
import android.app.PictureInPictureParams
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@ -36,6 +37,7 @@ class MainActivity : AppCompatActivity() {
var isInPlayer: Boolean = false
var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false
lateinit var mainContext : MainActivity
}
private fun enterPIPMode() {
@ -83,6 +85,8 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainContext = this
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)

View file

@ -22,9 +22,10 @@ import kotlinx.android.synthetic.main.result_episode.view.episode_holder
import kotlinx.android.synthetic.main.result_episode.view.episode_text
import kotlinx.android.synthetic.main.result_episode_large.view.*
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
const val ACTION_RELOAD_EPISODE = 4
const val ACTION_CHROME_CAST_EPISODE = 2
const val ACTION_DOWNLOAD_EPISODE = 3
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
@ -145,10 +146,12 @@ class EpisodeAdapter(
if (castContext.castState == CastState.CONNECTED) {
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))
}
}
}

View file

@ -45,7 +45,9 @@ import com.lagradost.cloudstream3.ui.player.PlayerData
import com.lagradost.cloudstream3.ui.player.PlayerFragment
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.android.synthetic.main.fragment_result.*
@ -258,7 +260,6 @@ class ResultFragment : Fragment() {
currentLoadingCount++
when (episodeClick.action) {
ACTION_CHROME_CAST_EPISODE -> {
val skipLoading = if (apiName != null) {
getApiFromName(apiName).instantLinkLoading
} else false
@ -327,7 +328,19 @@ class ResultFragment : Fragment() {
}
}*/
}
ACTION_DOWNLOAD_EPISODE -> {
val tempUrl = url
if (tempUrl != null) {
viewModel.loadEpisode(episodeClick.data, true) { data ->
if (data is Resource.Success) {
VideoDownloadManager.DownloadEpisode(requireContext(),
tempUrl,
episodeClick.data,
data.value)
}
}
}
}
}
}

View file

@ -148,7 +148,7 @@ class SearchFragment : Fragment() {
allApi.providersActive = requireActivity().getApiSettings()
//searchViewModel.search("iron man")
//(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro")
(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro")
/*
(requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_anim,

View file

@ -0,0 +1,310 @@
package com.lagradost.cloudstream3.utils
import android.app.NotificationChannel
import android.app.NotificationManager
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.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
const val CHANNEL_ID = "cloudstream3.general"
const val CHANNEL_NAME = "Downloads"
const val CHANNEL_DESCRIPT = "The download notification channel"
object VideoDownloadManager {
@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,
}
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) {
val name = CHANNEL_NAME //getString(R.string.channel_name)
val descriptionText = CHANNEL_DESCRIPT//getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system
val notificationManager: NotificationManager =
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private val cachedBitmaps = hashMapOf<String, Bitmap>()
private fun Context.getImageBitmapFromUrl(url: String): Bitmap? {
if (cachedBitmaps.containsKey(url)) {
return cachedBitmaps[url]
}
val bitmap = Glide.with(this)
.asBitmap()
.load(url).into(720, 720)
.get()
if (bitmap != null) {
cachedBitmaps[url] = bitmap
}
return null
}
fun createNotification(
context: Context,
text: String,
source: String,
ep: ResultEpisode,
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)
.setSmallIcon(
when (state) {
DownloadType.IsDone -> imgDone
DownloadType.IsDownloading -> imgDownloading
DownloadType.IsPaused -> imgPaused
DownloadType.IsFailed -> imgError
DownloadType.IsStopped -> imgStopped
}
)
.setContentIntent(pendingIntent)
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
builder.setProgress(total.toInt(), progress.toInt(), false)
}
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)
}
}
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)
}
if (state == DownloadType.IsPaused) {
actionTypes.add(DownloadActionType.Resume)
actionTypes.add(DownloadActionType.Stop)
}
// ADD ACTIONS
for ((index, i) in actionTypes.withIndex()) {
val _resultIntent = Intent(context, DownloadService::class.java)
_resultIntent.putExtra(
"type", when (i) {
DownloadActionType.Resume -> "resume"
DownloadActionType.Pause -> "pause"
DownloadActionType.Stop -> "stop"
}
)
_resultIntent.putExtra("id", ep.id)
val pending: PendingIntent = PendingIntent.getService(
context, 4337 + index + ep.id,
_resultIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
builder.addAction(
NotificationCompat.Action(
when (i) {
DownloadActionType.Resume -> pressToResumeIcon
DownloadActionType.Pause -> pressToPauseIcon
DownloadActionType.Stop -> pressToStopIcon
}, when (i) {
DownloadActionType.Resume -> "Resume"
DownloadActionType.Pause -> "Pause"
DownloadActionType.Stop -> "Stop"
}, pending
)
)
}
}
if (!hasCreatedNotChanel) {
context.createNotificationChannel()
}
with(NotificationManagerCompat.from(context)) {
// notificationId is a unique int for each notification that you must define
notify(ep.id, builder.build())
}
}
//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)
// Note: This should be a singleton in your app.
val databaseProvider = ExoDatabaseProvider(context)
val downloadDirectory = File(Environment.getExternalStorageDirectory().path + "/Download/" + (ep.name ?: "Episode ${ep.episode}")) // File(context.cacheDir, "video_${ep.id}")
// A download cache should not evict media, so should use a NoopCacheEvictor.
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
)
}
)*/
}
public fun DownloadEpisode(context: Context, source: String, ep: ResultEpisode, links: List<ExtractorLink>) {
val validLinks = links.filter { !it.isM3u8 }
if (validLinks.isNotEmpty()) {
DownloadSingleEpisode(context, source, ep, validLinks.first())
}
}
}

View file

@ -0,0 +1,97 @@
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()
}
}

View file

@ -0,0 +1,33 @@
package com.lagradost.cloudstream3.utils
import com.google.android.exoplayer2.database.ExoDatabaseProvider
import com.google.android.exoplayer2.offline.DownloadManager
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.lagradost.cloudstream3.MainActivity
import java.util.concurrent.Executor
object ExoPlayerHelper {
private val context = MainActivity.mainContext
val databaseProvider = ExoDatabaseProvider(context)
val downloadExecutor = Executor { obj: Runnable -> obj.run() }
val dataSourceFactory = DefaultHttpDataSourceFactory()
val downloadCache: SimpleCache by lazy {
SimpleCache(
context.cacheDir,
LeastRecentlyUsedCacheEvictor(20 * 1024 * 1024),
databaseProvider
)
}
val downloadManager: DownloadManager by lazy {
DownloadManager(context,
databaseProvider,
downloadCache,
dataSourceFactory,
downloadExecutor)
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/drawable.iml" filepath="$PROJECT_DIR$/.idea/drawable.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../../../.." vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,24 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 17 1.01 L 7 1 C 5.9 1 5 1.9 5 3 L 5 21 C 5 22.1 5.9 23 7 23 L 17 23 C 18.1 23 19 22.1 19 21 L 19 3 C 19 1.9 18.1 1.01 17 1.01 Z M 17 19 L 7 19 L 7 5 L 17 5 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 7.488 13.004 L 9.132 11.36 L 12.492 14.72 L 10.848 16.364 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
<path
android:name="path_2"
android:pathData="M 10.848 16.364 L 9.203 14.72 L 14.894 9.029 L 16.539 10.673 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,24 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 17 1.01 L 7 1 C 5.9 1 5 1.9 5 3 L 5 21 C 5 22.1 5.9 23 7 23 L 17 23 C 18.1 23 19 22.1 19 21 L 19 3 C 19 1.9 18.1 1.01 17 1.01 Z M 17 19 L 7 19 L 7 5 L 17 5 Z M 10.101 13.289 L 10.513 10.311 L 9.848 9.33 L 9.496 8.867 L 11 13 L 9.706 13 L 9.745 15.93 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 15.801 14.763 L 14.157 16.407 L 7.873 10.124 L 9.518 8.479 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
<path
android:name="path_2"
android:pathData="M 14.157 8.479 L 15.801 10.124 L 9.518 16.407 L 7.873 14.763 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorPrimary"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19L7,19L7,5h10v14zM16,13h-3L13,8h-2v5L8,13l4,4 4,-4z"/>
</vector>

View file

@ -0,0 +1,19 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 17 1.01 L 7 1 C 5.9 1 5 1.9 5 3 L 5 21 C 5 22.1 5.9 23 7 23 L 17 23 C 18.1 23 19 22.1 19 21 L 19 3 C 19 1.9 18.1 1.01 17 1.01 Z M 17 19 L 7 19 L 7 5 L 17 5 Z M 10.101 13.289 L 10.513 10.311 L 9.848 9.33 L 9.496 8.867 L 11 13 L 9.706 13 L 9.745 15.93 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 9.104 8.931 L 11.129 8.931 L 11.129 15.956 L 9.104 15.956 Z M 12.871 8.931 L 14.896 8.931 L 14.896 15.956 L 12.871 15.956 Z"
android:fillColor="@color/white"
android:strokeWidth="1"/>
</vector>