mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
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.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -9,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.anggrayudi.storage.SimpleStorage
|
||||
import com.google.android.gms.cast.ApplicationMetadata
|
||||
import com.google.android.gms.cast.Cast
|
||||
import com.google.android.gms.cast.LaunchOptions
|
||||
|
@ -38,8 +40,16 @@ class MainActivity : AppCompatActivity() {
|
|||
var canShowPipMode: Boolean = false
|
||||
var isInPIPMode: Boolean = false
|
||||
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() {
|
||||
if (!shouldShowPIPMode(isInPlayer) || !canShowPipMode) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
@ -83,9 +93,32 @@ class MainActivity : AppCompatActivity() {
|
|||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mainContext = this
|
||||
setupSimpleStorage()
|
||||
storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS)
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
||||
|
@ -100,8 +133,11 @@ class MainActivity : AppCompatActivity() {
|
|||
val navController = findNavController(R.id.nav_host_fragment)
|
||||
// Passing each menu ID as a set of Ids because each
|
||||
// menu should be considered as top level destinations.
|
||||
val appBarConfiguration = AppBarConfiguration(setOf(
|
||||
R.id.navigation_home, R.id.navigation_search, R.id.navigation_notifications))
|
||||
val appBarConfiguration = AppBarConfiguration(
|
||||
setOf(
|
||||
R.id.navigation_home, R.id.navigation_search, R.id.navigation_notifications
|
||||
)
|
||||
)
|
||||
//setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
navView.setupWithNavController(navController)
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ class EpisodeAdapter(
|
|||
}
|
||||
} else {
|
||||
// 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 var allEpisodes: HashMap<Int, ArrayList<ExtractorLink>> = HashMap()
|
||||
private var currentHeaderName: String? = null
|
||||
private var currentType: TvType? = null
|
||||
private var currentEpisodes: List<ResultEpisode>? = null
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -341,18 +342,30 @@ class ResultFragment : Fragment() {
|
|||
if (tempUrl != null) {
|
||||
viewModel.loadEpisode(episodeClick.data, true) { data ->
|
||||
if (data is Resource.Success) {
|
||||
val isMovie = currentIsMovie ?: return@loadEpisode
|
||||
val titleName = currentHeaderName?: return@loadEpisode
|
||||
val meta = VideoDownloadManager.DownloadEpisodeMetadata(
|
||||
episodeClick.data.id,
|
||||
currentHeaderName ?: return@loadEpisode,
|
||||
titleName ,
|
||||
apiName ?: return@loadEpisode,
|
||||
episodeClick.data.poster,
|
||||
episodeClick.data.poster ?: currentPoster,
|
||||
episodeClick.data.name,
|
||||
episodeClick.data.season,
|
||||
episodeClick.data.episode
|
||||
if (isMovie) null else episodeClick.data.season,
|
||||
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(
|
||||
requireContext(),
|
||||
tempUrl,
|
||||
folder,
|
||||
meta,
|
||||
data.value.links
|
||||
)
|
||||
|
@ -435,6 +448,7 @@ class ResultFragment : Fragment() {
|
|||
result_bookmark_button.text = "Watching"
|
||||
|
||||
currentHeaderName = d.name
|
||||
currentType = d.type
|
||||
|
||||
currentPoster = d.posterUrl
|
||||
currentIsMovie = !d.isEpisodeBased()
|
||||
|
@ -610,10 +624,15 @@ activity?.startActivityForResult(vlcIntent, REQUEST_CODE)
|
|||
if (castContext.castState == CastState.CONNECTED) {
|
||||
handleAction(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card))
|
||||
} else {
|
||||
handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card))
|
||||
handleAction(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
|
||||
}
|
||||
} 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.R
|
||||
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.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
|
||||
|
||||
const val CHANNEL_ID = "cloudstream3.general"
|
||||
const val CHANNEL_NAME = "Downloads"
|
||||
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(
|
||||
context: Context,
|
||||
source: String?,
|
||||
folder: 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
|
||||
val path = sanitizeFilename("Download/${if (folder == null) "" else "$folder/"}$name")
|
||||
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 (!dFile.delete()) {
|
||||
if (!fileExists) resume = false
|
||||
if (fileExists && !resume) {
|
||||
if (tempFile?.delete() == false) { // DELETE FAILED ON RESUME FILE
|
||||
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
|
||||
|
@ -306,8 +330,16 @@ object VideoDownloadManager {
|
|||
// ON CONNECTION
|
||||
connection.connect()
|
||||
val contentLength = connection.contentLength
|
||||
if (contentLength < 5000000) return false // less than 5mb
|
||||
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
|
||||
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
|
||||
|
@ -315,6 +347,7 @@ object VideoDownloadManager {
|
|||
var count: Int
|
||||
var bytesDownloaded = resumeLength
|
||||
|
||||
// TO NOT REUSE CODE
|
||||
fun updateNotification(type: DownloadType) {
|
||||
createNotification(
|
||||
context,
|
||||
|
@ -327,7 +360,7 @@ object VideoDownloadManager {
|
|||
)
|
||||
}
|
||||
|
||||
while (true) {
|
||||
while (true) { // TODO PAUSE
|
||||
count = connectionInputStream.read(buffer)
|
||||
if (count < 0) break
|
||||
bytesDownloaded += count
|
||||
|
@ -344,10 +377,25 @@ object VideoDownloadManager {
|
|||
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 }
|
||||
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