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.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…
	
	Add table
		Add a link
		
	
		Reference in a new issue