forked from recloudstream/cloudstream
download step 1
This commit is contained in:
parent
602cd065ce
commit
3210e71eca
5 changed files with 142 additions and 23 deletions
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -9,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.google.android.gms.cast.ApplicationMetadata
|
import com.google.android.gms.cast.ApplicationMetadata
|
||||||
import com.google.android.gms.cast.Cast
|
import com.google.android.gms.cast.Cast
|
||||||
import com.google.android.gms.cast.LaunchOptions
|
import com.google.android.gms.cast.LaunchOptions
|
||||||
|
@ -37,9 +39,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
var isInPlayer: Boolean = false
|
var isInPlayer: Boolean = false
|
||||||
var canShowPipMode: Boolean = false
|
var canShowPipMode: Boolean = false
|
||||||
var isInPIPMode: Boolean = false
|
var isInPIPMode: Boolean = false
|
||||||
lateinit var mainContext : MainActivity
|
lateinit var mainContext: MainActivity
|
||||||
|
|
||||||
|
//https://github.com/anggrayudi/SimpleStorage/blob/4eb6306efb6cdfae4e34f170c8b9d4e135b04d51/sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt#L624
|
||||||
|
const val REQUEST_CODE_STORAGE_ACCESS = 1
|
||||||
|
const val REQUEST_CODE_PICK_FOLDER = 2
|
||||||
|
const val REQUEST_CODE_PICK_FILE = 3
|
||||||
|
const val REQUEST_CODE_ASK_PERMISSIONS = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lateinit var storage: SimpleStorage
|
||||||
|
|
||||||
private fun enterPIPMode() {
|
private fun enterPIPMode() {
|
||||||
if (!shouldShowPIPMode(isInPlayer) || !canShowPipMode) return
|
if (!shouldShowPIPMode(isInPlayer) || !canShowPipMode) return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
@ -83,9 +93,32 @@ class MainActivity : AppCompatActivity() {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun setupSimpleStorage() {
|
||||||
|
storage = SimpleStorage(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
// Mandatory for Activity, but not for Fragment
|
||||||
|
storage.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
storage.onSaveInstanceState(outState)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
storage.onRestoreInstanceState(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mainContext = this
|
mainContext = this
|
||||||
|
setupSimpleStorage()
|
||||||
|
storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS)
|
||||||
|
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
||||||
|
@ -100,8 +133,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
val navController = findNavController(R.id.nav_host_fragment)
|
val navController = findNavController(R.id.nav_host_fragment)
|
||||||
// Passing each menu ID as a set of Ids because each
|
// Passing each menu ID as a set of Ids because each
|
||||||
// menu should be considered as top level destinations.
|
// menu should be considered as top level destinations.
|
||||||
val appBarConfiguration = AppBarConfiguration(setOf(
|
val appBarConfiguration = AppBarConfiguration(
|
||||||
R.id.navigation_home, R.id.navigation_search, R.id.navigation_notifications))
|
setOf(
|
||||||
|
R.id.navigation_home, R.id.navigation_search, R.id.navigation_notifications
|
||||||
|
)
|
||||||
|
)
|
||||||
//setupActionBarWithNavController(navController, appBarConfiguration)
|
//setupActionBarWithNavController(navController, appBarConfiguration)
|
||||||
navView.setupWithNavController(navController)
|
navView.setupWithNavController(navController)
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ class EpisodeAdapter(
|
||||||
}
|
}
|
||||||
} else {
|
} 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))
|
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card)) //TODO REDO TO MAIN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,7 @@ class ResultFragment : Fragment() {
|
||||||
private lateinit var viewModel: ResultViewModel
|
private lateinit var viewModel: ResultViewModel
|
||||||
private var allEpisodes: HashMap<Int, ArrayList<ExtractorLink>> = HashMap()
|
private var allEpisodes: HashMap<Int, ArrayList<ExtractorLink>> = HashMap()
|
||||||
private var currentHeaderName: String? = null
|
private var currentHeaderName: String? = null
|
||||||
|
private var currentType: TvType? = null
|
||||||
private var currentEpisodes: List<ResultEpisode>? = null
|
private var currentEpisodes: List<ResultEpisode>? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -341,18 +342,30 @@ 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 isMovie = currentIsMovie ?: return@loadEpisode
|
||||||
|
val titleName = currentHeaderName?: return@loadEpisode
|
||||||
val meta = VideoDownloadManager.DownloadEpisodeMetadata(
|
val meta = VideoDownloadManager.DownloadEpisodeMetadata(
|
||||||
episodeClick.data.id,
|
episodeClick.data.id,
|
||||||
currentHeaderName ?: return@loadEpisode,
|
titleName ,
|
||||||
apiName ?: return@loadEpisode,
|
apiName ?: return@loadEpisode,
|
||||||
episodeClick.data.poster,
|
episodeClick.data.poster ?: currentPoster,
|
||||||
episodeClick.data.name,
|
episodeClick.data.name,
|
||||||
episodeClick.data.season,
|
if (isMovie) null else episodeClick.data.season,
|
||||||
episodeClick.data.episode
|
if (isMovie) null else episodeClick.data.episode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val folder = when (currentType) {
|
||||||
|
TvType.Anime -> "Anime/$titleName"
|
||||||
|
TvType.Movie -> "Movies"
|
||||||
|
TvType.TvSeries -> "TVSeries/$titleName"
|
||||||
|
TvType.ONA -> "ONA"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
VideoDownloadManager.DownloadEpisode(
|
VideoDownloadManager.DownloadEpisode(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
tempUrl,
|
tempUrl,
|
||||||
|
folder,
|
||||||
meta,
|
meta,
|
||||||
data.value.links
|
data.value.links
|
||||||
)
|
)
|
||||||
|
@ -435,6 +448,7 @@ class ResultFragment : Fragment() {
|
||||||
result_bookmark_button.text = "Watching"
|
result_bookmark_button.text = "Watching"
|
||||||
|
|
||||||
currentHeaderName = d.name
|
currentHeaderName = d.name
|
||||||
|
currentType = d.type
|
||||||
|
|
||||||
currentPoster = d.posterUrl
|
currentPoster = d.posterUrl
|
||||||
currentIsMovie = !d.isEpisodeBased()
|
currentIsMovie = !d.isEpisodeBased()
|
||||||
|
@ -610,10 +624,15 @@ activity?.startActivityForResult(vlcIntent, REQUEST_CODE)
|
||||||
if (castContext.castState == CastState.CONNECTED) {
|
if (castContext.castState == CastState.CONNECTED) {
|
||||||
handleAction(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card))
|
handleAction(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card))
|
||||||
} else {
|
} else {
|
||||||
handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card))
|
handleAction(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card))
|
handleAction(
|
||||||
|
EpisodeClickEvent(
|
||||||
|
ACTION_DOWNLOAD_EPISODE,
|
||||||
|
card
|
||||||
|
)
|
||||||
|
) //TODO REDO TO MAIN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,14 @@ import com.google.android.exoplayer2.offline.DownloadService
|
||||||
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.utils.Coroutines.main
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
|
||||||
const val CHANNEL_ID = "cloudstream3.general"
|
const val CHANNEL_ID = "cloudstream3.general"
|
||||||
const val CHANNEL_NAME = "Downloads"
|
const val CHANNEL_NAME = "Downloads"
|
||||||
const val CHANNEL_DESCRIPT = "The download notification channel"
|
const val CHANNEL_DESCRIPT = "The download notification channel"
|
||||||
|
@ -268,25 +270,47 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val reservedChars = "|\\?*<\":>+[]/'"
|
||||||
|
private fun sanitizeFilename(name: String): String {
|
||||||
|
var tempName = name
|
||||||
|
for (c in reservedChars) {
|
||||||
|
tempName = tempName.replace(c, ' ')
|
||||||
|
}
|
||||||
|
return tempName.replace(" ", " ")
|
||||||
|
}
|
||||||
|
|
||||||
fun DownloadSingleEpisode(
|
fun DownloadSingleEpisode(
|
||||||
context: Context,
|
context: Context,
|
||||||
source: String?,
|
source: String?,
|
||||||
|
folder: String?,
|
||||||
ep: DownloadEpisodeMetadata,
|
ep: DownloadEpisodeMetadata,
|
||||||
link: ExtractorLink
|
link: ExtractorLink
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val name = (ep.name ?: "Episode ${ep.episode}")
|
val name = (ep.name ?: "Episode ${ep.episode}")
|
||||||
val path = "Downloads/Anime/$name.mp4"
|
val path = sanitizeFilename("Download/${if (folder == null) "" else "$folder/"}$name")
|
||||||
val dFile = DocumentFileCompat.fromSimplePath(context, basePath = path) ?: return false
|
var resume = false
|
||||||
|
|
||||||
val resume = false
|
// IF RESUME, DELETE FILE IF FOUND AND RECREATE
|
||||||
|
// IF NOT RESUME CREATE FILE
|
||||||
|
val tempFile = DocumentFileCompat.fromSimplePath(context, basePath = path)
|
||||||
|
val fileExists = tempFile?.exists() ?: false
|
||||||
|
|
||||||
if (!resume && dFile.exists()) {
|
if (!fileExists) resume = false
|
||||||
if (!dFile.delete()) {
|
if (fileExists && !resume) {
|
||||||
|
if (tempFile?.delete() == false) { // DELETE FAILED ON RESUME FILE
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!dFile.exists()) {
|
|
||||||
dFile.createFile("video/mp4", name)
|
val dFile =
|
||||||
|
if (resume) tempFile
|
||||||
|
else DocumentFileCompat.createFile(context, basePath = path, mimeType = "video/mp4")
|
||||||
|
|
||||||
|
// END OF FILE CREATION
|
||||||
|
|
||||||
|
if (dFile == null) {
|
||||||
|
println("FUCK YOU")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPEN FILE
|
// OPEN FILE
|
||||||
|
@ -306,8 +330,16 @@ object VideoDownloadManager {
|
||||||
// ON CONNECTION
|
// ON CONNECTION
|
||||||
connection.connect()
|
connection.connect()
|
||||||
val contentLength = connection.contentLength
|
val contentLength = connection.contentLength
|
||||||
if (contentLength < 5000000) return false // less than 5mb
|
|
||||||
val bytesTotal = contentLength + resumeLength
|
val bytesTotal = contentLength + resumeLength
|
||||||
|
if (bytesTotal < 5000000) return false // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if(!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) {
|
||||||
|
return false // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE
|
||||||
|
}
|
||||||
|
|
||||||
// READ DATA FROM CONNECTION
|
// READ DATA FROM CONNECTION
|
||||||
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
|
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
|
||||||
|
@ -315,7 +347,8 @@ object VideoDownloadManager {
|
||||||
var count: Int
|
var count: Int
|
||||||
var bytesDownloaded = resumeLength
|
var bytesDownloaded = resumeLength
|
||||||
|
|
||||||
fun updateNotification(type : DownloadType) {
|
// TO NOT REUSE CODE
|
||||||
|
fun updateNotification(type: DownloadType) {
|
||||||
createNotification(
|
createNotification(
|
||||||
context,
|
context,
|
||||||
source,
|
source,
|
||||||
|
@ -327,7 +360,7 @@ object VideoDownloadManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) { // TODO PAUSE
|
||||||
count = connectionInputStream.read(buffer)
|
count = connectionInputStream.read(buffer)
|
||||||
if (count < 0) break
|
if (count < 0) break
|
||||||
bytesDownloaded += count
|
bytesDownloaded += count
|
||||||
|
@ -344,10 +377,25 @@ object VideoDownloadManager {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun DownloadEpisode(context: Context, source: String, ep: DownloadEpisodeMetadata, links: List<ExtractorLink>) {
|
public fun DownloadEpisode(
|
||||||
|
context: Context,
|
||||||
|
source: String,
|
||||||
|
folder: 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())
|
try {
|
||||||
|
main {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
DownloadSingleEpisode(context, source, folder, ep, validLinks.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println(e)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
|
import android.app.IntentService
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
class VideoDownloadService : IntentService("DownloadService") {
|
||||||
|
override fun onHandleIntent(intent: Intent?) {
|
||||||
|
if (intent != null) {
|
||||||
|
val id = intent.getIntExtra("id", -1)
|
||||||
|
val type = intent.getStringExtra("type")
|
||||||
|
if (id != -1 && type != null) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue