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 | ||||
|  | @ -37,9 +39,17 @@ class MainActivity : AppCompatActivity() { | |||
|         var isInPlayer: Boolean = false | ||||
|         var canShowPipMode: 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() { | ||||
|         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,7 +347,8 @@ object VideoDownloadManager { | |||
|         var count: Int | ||||
|         var bytesDownloaded = resumeLength | ||||
| 
 | ||||
|         fun updateNotification(type : DownloadType) { | ||||
|         // TO NOT REUSE CODE | ||||
|         fun updateNotification(type: DownloadType) { | ||||
|             createNotification( | ||||
|                 context, | ||||
|                 source, | ||||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue