diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5a97be8f..1cf5388e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 4eb3a7fb..f7fc43d3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -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 } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 8f5657f0..7867c6b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -134,6 +134,7 @@ class ResultFragment : Fragment() { private lateinit var viewModel: ResultViewModel private var allEpisodes: HashMap> = HashMap() private var currentHeaderName: String? = null + private var currentType: TvType? = null private var currentEpisodes: List? = 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 } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt similarity index 82% rename from app/src/main/java/com/lagradost/cloudstream3/utils/DownloadManager.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index b3ebba2c..8651ca9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -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) { + public fun DownloadEpisode( + context: Context, + source: String, + folder: String?, + ep: DownloadEpisodeMetadata, + links: List + ) { 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() + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadService.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadService.kt new file mode 100644 index 00000000..a365313f --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadService.kt @@ -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) { + + } + } + } +} \ No newline at end of file