mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Added custom download directory (#219)
Co-authored-by: Osten <balt.758@gmail.com>
This commit is contained in:
		
							parent
							
								
									13ba73bbd4
								
							
						
					
					
						commit
						e5f3d4b20b
					
				
					 10 changed files with 390 additions and 116 deletions
				
			
		|  | @ -98,6 +98,7 @@ object DownloadButtonSetup { | |||
|                         R.id.global_to_navigation_player, PlayerFragment.newInstance( | ||||
|                             UriData( | ||||
|                                 info.path.toString(), | ||||
|                                 keyInfo.basePath, | ||||
|                                 keyInfo.relativePath, | ||||
|                                 keyInfo.displayName, | ||||
|                                 click.data.parentId, | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import com.lagradost.cloudstream3.isMovieType | |||
| import com.lagradost.cloudstream3.mvvm.observe | ||||
| import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.loadResult | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.main | ||||
| import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE | ||||
| import com.lagradost.cloudstream3.utils.DataStore | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||
|  | @ -44,8 +45,10 @@ class DownloadFragment : Fragment() { | |||
|     } | ||||
| 
 | ||||
|     private fun setList(list: List<VisualDownloadHeaderCached>) { | ||||
|         (download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list | ||||
|         download_list?.adapter?.notifyDataSetChanged() | ||||
|         main { | ||||
|             (download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list | ||||
|             download_list?.adapter?.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroyView() { | ||||
|  |  | |||
|  | @ -113,4 +113,4 @@ class DownloadViewModel : ViewModel() { | |||
| 
 | ||||
|         _headerCards.postValue(visual) | ||||
|     } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -78,7 +78,6 @@ import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAu | |||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle | ||||
| import com.lagradost.cloudstream3.utils.* | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.getVideoContentUri | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.onAudioFocusEvent | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus | ||||
|  | @ -108,6 +107,7 @@ import kotlin.math.abs | |||
| import kotlin.math.ceil | ||||
| import kotlin.properties.Delegates | ||||
| 
 | ||||
| 
 | ||||
| //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 | ||||
| const val STATE_RESUME_WINDOW = "resumeWindow" | ||||
| const val STATE_RESUME_POSITION = "resumePosition" | ||||
|  | @ -158,6 +158,7 @@ data class PlayerData( | |||
| 
 | ||||
| data class UriData( | ||||
|     val uri: String, | ||||
|     val basePath: String?, | ||||
|     val relativePath: String, | ||||
|     val displayName: String, | ||||
|     val parentId: Int?, | ||||
|  | @ -791,12 +792,12 @@ class PlayerFragment : Fragment() { | |||
|             if (exoPlayer.duration > 0 && exoPlayer.currentPosition > 0) { | ||||
|                 context?.let { ctx -> | ||||
|                     //if (this::viewModel.isInitialized) { | ||||
|                         viewModel.setViewPos( | ||||
|                             ctx, | ||||
|                             if (isDownloadedFile) uriData.id else getEpisode()?.id, | ||||
|                             exoPlayer.currentPosition, | ||||
|                             exoPlayer.duration | ||||
|                         ) | ||||
|                     viewModel.setViewPos( | ||||
|                         ctx, | ||||
|                         if (isDownloadedFile) uriData.id else getEpisode()?.id, | ||||
|                         exoPlayer.currentPosition, | ||||
|                         exoPlayer.duration | ||||
|                     ) | ||||
|                     /*} else { | ||||
|                         ctx.setViewPos( | ||||
|                             if (isDownloadedFile) uriData.id else getEpisode()?.id, | ||||
|  | @ -1608,7 +1609,7 @@ class PlayerFragment : Fragment() { | |||
|             if (isDownloadedFile) { | ||||
|                 if (!supportsDownloadedFiles) return null | ||||
|                 val list = ArrayList<SubtitleFile>() | ||||
|                 VideoDownloadManager.getFolder(this, uriData.relativePath)?.forEach { file -> | ||||
|                 VideoDownloadManager.getFolder(this, uriData.relativePath, uriData.basePath)?.forEach { file -> | ||||
|                     val name = uriData.displayName.removeSuffix(".mp4") | ||||
|                     if (file.first != uriData.displayName && file.first.startsWith(name)) { | ||||
|                         val realName = file.first.removePrefix(name) | ||||
|  | @ -1882,15 +1883,7 @@ class PlayerFragment : Fragment() { | |||
|                 mediaItemBuilder.setUri(currentUrl.url) | ||||
|             } else if (trueUri != null || uri != null) { | ||||
|                 val uriPrimary = trueUri ?: Uri.parse(uri) | ||||
|                 if (uriPrimary.scheme == "content") { | ||||
|                     mediaItemBuilder.setUri(uriPrimary) | ||||
|                     //      video_title?.text = uriPrimary.toString() | ||||
|                 } else { | ||||
|                     //mediaItemBuilder.setUri(Uri.parse(currentUrl.url)) | ||||
|                     val realUri = trueUri ?: getVideoContentUri(requireContext(), uri ?: uriPrimary.path ?: "") | ||||
|                     //    video_title?.text = uri.toString() | ||||
|                     mediaItemBuilder.setUri(realUri) | ||||
|                 } | ||||
|                 mediaItemBuilder.setUri(uriPrimary) | ||||
|             } | ||||
| 
 | ||||
|             val subs = context?.getSubs() ?: emptyList() | ||||
|  | @ -1913,7 +1906,7 @@ class PlayerFragment : Fragment() { | |||
|             activeSubtitles = subItemsId | ||||
|             mediaItemBuilder.setSubtitles(subItems) | ||||
| 
 | ||||
| //might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps | ||||
|             //might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps | ||||
| 
 | ||||
|             val mediaItem = mediaItemBuilder.build() | ||||
|             val trackSelector = DefaultTrackSelector(requireContext()) | ||||
|  | @ -2214,7 +2207,8 @@ class PlayerFragment : Fragment() { | |||
|     @SuppressLint("ClickableViewAccessibility") | ||||
|     private fun initPlayer() { | ||||
|         if (isDownloadedFile) { | ||||
|             initPlayer(null, uriData.uri.removePrefix("file://").replace("%20", " ")) // FIX FILE PERMISSION | ||||
|             //.removePrefix("file://").replace("%20", " ") // FIX FILE PERMISSION | ||||
|             initPlayer(null, uriData.uri) | ||||
|         } | ||||
|         println("INIT PLAYER") | ||||
|         view?.setOnTouchListener { _, _ -> return@setOnTouchListener true } // VERY IMPORTANT https://stackoverflow.com/questions/28818926/prevent-clicking-on-a-button-in-an-activity-while-showing-a-fragment | ||||
|  |  | |||
|  | @ -1,19 +1,26 @@ | |||
| package com.lagradost.cloudstream3.ui.settings | ||||
| 
 | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.app.UiModeManager | ||||
| import android.content.Context | ||||
| import android.content.res.Configuration | ||||
| import android.os.Bundle | ||||
| import android.os.Environment | ||||
| import android.widget.Toast | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.preference.Preference | ||||
| import androidx.preference.PreferenceFragmentCompat | ||||
| import androidx.preference.PreferenceManager | ||||
| import com.hippo.unifile.UniFile | ||||
| import com.lagradost.cloudstream3.APIHolder.apis | ||||
| import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings | ||||
| import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings | ||||
| import com.lagradost.cloudstream3.APIHolder.getApiSettings | ||||
| import com.lagradost.cloudstream3.APIHolder.restrictedApis | ||||
| import com.lagradost.cloudstream3.AcraApplication | ||||
| import com.lagradost.cloudstream3.DubStatus | ||||
| import com.lagradost.cloudstream3.MainActivity.Companion.setLocale | ||||
| import com.lagradost.cloudstream3.MainActivity.Companion.showToast | ||||
|  | @ -32,6 +39,10 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog | |||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog | ||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadDir | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.isScopedStorage | ||||
| import java.io.File | ||||
| import kotlin.concurrent.thread | ||||
| 
 | ||||
| 
 | ||||
|  | @ -54,6 +65,33 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
| 
 | ||||
|     private var beneneCount = 0 | ||||
| 
 | ||||
|     // Open file picker | ||||
|     private val pathPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> | ||||
|         // It lies, it can be null if file manager quits. | ||||
|         if (uri == null) return@registerForActivityResult | ||||
|         val context = context ?: AcraApplication.context ?: return@registerForActivityResult | ||||
|         // RW perms for the path | ||||
|         val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or | ||||
|                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION | ||||
| 
 | ||||
|         context.contentResolver.takePersistableUriPermission(uri, flags) | ||||
| 
 | ||||
|         val file = UniFile.fromUri(context, uri) | ||||
|         println("Selected URI path: $uri - Full path: ${file.filePath}") | ||||
| 
 | ||||
|         // Stores the real URI using download_path_key | ||||
|         // Important that the URI is stored instead of filepath due to permissions. | ||||
|         PreferenceManager.getDefaultSharedPreferences(context) | ||||
|             .edit().putString(getString(R.string.download_path_key), uri.toString()).apply() | ||||
| 
 | ||||
|         // From URI -> File path | ||||
|         // File path here is purely for cosmetic purposes in settings | ||||
|         (file.filePath ?: uri.toString()).let { | ||||
|             PreferenceManager.getDefaultSharedPreferences(context) | ||||
|                 .edit().putString(getString(R.string.download_path_pref), it).apply() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // idk, if you find a way of automating this it would be great | ||||
|     private val languages = arrayListOf( | ||||
|         Triple("\uD83C\uDDEA\uD83C\uDDF8", "Spanish", "es"), | ||||
|  | @ -84,6 +122,7 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|         val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!! | ||||
|         val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!! | ||||
|         val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!! | ||||
|         val downloadPathPreference = findPreference<Preference>(getString(R.string.download_path_key))!! | ||||
|         val allLayoutPreference = findPreference<Preference>(getString(R.string.app_layout_key))!! | ||||
|         val colorPrimaryPreference = findPreference<Preference>(getString(R.string.primary_color_key))!! | ||||
|         val preferedMediaTypePreference = findPreference<Preference>(getString(R.string.prefer_media_type_key))!! | ||||
|  | @ -167,7 +206,49 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
| 
 | ||||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
|          | ||||
|         fun getDownloadDirs(): List<String> { | ||||
|             val defaultDir = getDownloadDir()?.filePath | ||||
| 
 | ||||
|             // app_name_download_path = Cloudstream and does not change depending on release. | ||||
|             // DOES NOT WORK ON SCOPED STORAGE. | ||||
|             val secondaryDir = if (isScopedStorage) null else Environment.getExternalStorageDirectory().absolutePath + | ||||
|                     File.separator + resources.getString(R.string.app_name_download_path) | ||||
| 
 | ||||
|             val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second } | ||||
| 
 | ||||
|             return (listOf(defaultDir, secondaryDir) + | ||||
|                     requireContext().getExternalFilesDirs("").mapNotNull { it.path } + | ||||
|                     currentDir).filterNotNull().distinct() | ||||
|         } | ||||
| 
 | ||||
|         downloadPathPreference.setOnPreferenceClickListener { | ||||
|             val dirs = getDownloadDirs() | ||||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) | ||||
| 
 | ||||
|             val currentDir = | ||||
|                 settingsManager.getString(getString(R.string.download_path_pref), null) ?: getDownloadDir().toString() | ||||
| 
 | ||||
|             context?.showBottomDialog( | ||||
|                 dirs + listOf("Custom"), | ||||
|                 dirs.indexOf(currentDir), | ||||
|                 getString(R.string.download_path_pref), | ||||
|                 true, | ||||
|                 {}) { | ||||
|                 // Last = custom | ||||
|                 if (it == dirs.size) { | ||||
|                     pathPicker.launch(Uri.EMPTY) | ||||
|                 } else { | ||||
|                     // Sets both visual and actual paths. | ||||
|                     // key = used path | ||||
|                     // pref = visual path | ||||
|                     settingsManager.edit().putString(getString(R.string.download_path_key), dirs[it]).apply() | ||||
|                     settingsManager.edit().putString(getString(R.string.download_path_pref), dirs[it]).apply() | ||||
|                 } | ||||
|             } | ||||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
|          | ||||
|         preferedMediaTypePreference.setOnPreferenceClickListener { | ||||
|             val prefNames = resources.getStringArray(R.array.media_type_pref) | ||||
|             val prefValues = resources.getIntArray(R.array.media_type_pref_values) | ||||
|  | @ -206,9 +287,10 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|                 true, | ||||
|                 {}) { | ||||
|                 try { | ||||
|                     settingsManager.edit().putInt(getString(R.string.app_layout_key), prefValues[it]).apply() | ||||
|                     settingsManager.edit().putInt(getString(R.string.app_layout_key), prefValues[it]) | ||||
|                         .apply() | ||||
|                     activity?.recreate() | ||||
|                 } catch (e : Exception) { | ||||
|                 } catch (e: Exception) { | ||||
|                     logError(e) | ||||
|                 } | ||||
|             } | ||||
|  | @ -220,7 +302,8 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|             val prefValues = resources.getStringArray(R.array.themes_overlay_names_values) | ||||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) | ||||
| 
 | ||||
|             val currentLayout = settingsManager.getString( getString(R.string.primary_color_key),prefValues.first()) | ||||
|             val currentLayout = | ||||
|                 settingsManager.getString(getString(R.string.primary_color_key), prefValues.first()) | ||||
|             context?.showBottomDialog( | ||||
|                 prefNames.toList(), | ||||
|                 prefValues.indexOf(currentLayout), | ||||
|  | @ -228,9 +311,10 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|                 true, | ||||
|                 {}) { | ||||
|                 try { | ||||
|                     settingsManager.edit().putString(getString(R.string.primary_color_key), prefValues[it]).apply() | ||||
|                     settingsManager.edit().putString(getString(R.string.primary_color_key), prefValues[it]) | ||||
|                         .apply() | ||||
|                     activity?.recreate() | ||||
|                 } catch (e : Exception) { | ||||
|                 } catch (e: Exception) { | ||||
|                     logError(e) | ||||
|                 } | ||||
|             } | ||||
|  | @ -242,7 +326,8 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|             val prefValues = resources.getStringArray(R.array.themes_names_values) | ||||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) | ||||
| 
 | ||||
|             val currentLayout = settingsManager.getString( getString(R.string.app_theme_key),prefValues.first()) | ||||
|             val currentLayout = | ||||
|                 settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) | ||||
|             context?.showBottomDialog( | ||||
|                 prefNames.toList(), | ||||
|                 prefValues.indexOf(currentLayout), | ||||
|  | @ -250,9 +335,10 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|                 true, | ||||
|                 {}) { | ||||
|                 try { | ||||
|                     settingsManager.edit().putString(getString(R.string.app_theme_key), prefValues[it]).apply() | ||||
|                     settingsManager.edit().putString(getString(R.string.app_theme_key), prefValues[it]) | ||||
|                         .apply() | ||||
|                     activity?.recreate() | ||||
|                 } catch (e : Exception) { | ||||
|                 } catch (e: Exception) { | ||||
|                     logError(e) | ||||
|                 } | ||||
|             } | ||||
|  | @ -265,14 +351,18 @@ class SettingsFragment : PreferenceFragmentCompat() { | |||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) | ||||
| 
 | ||||
|             val currentQuality = | ||||
|                 settingsManager.getInt(getString(R.string.watch_quality_pref), Qualities.values().last().value) | ||||
|                 settingsManager.getInt( | ||||
|                     getString(R.string.watch_quality_pref), | ||||
|                     Qualities.values().last().value | ||||
|                 ) | ||||
|             context?.showBottomDialog( | ||||
|                 prefNames.toList(), | ||||
|                 prefValues.indexOf(currentQuality), | ||||
|                 getString(R.string.watch_quality_pref), | ||||
|                 true, | ||||
|                 {}) { | ||||
|                 settingsManager.edit().putInt(getString(R.string.watch_quality_pref), prefValues[it]).apply() | ||||
|                 settingsManager.edit().putInt(getString(R.string.watch_quality_pref), prefValues[it]) | ||||
|                     .apply() | ||||
|             } | ||||
|             return@setOnPreferenceClickListener true | ||||
|         } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import android.content.Context | |||
| import androidx.work.CoroutineWorker | ||||
| import androidx.work.ForegroundInfo | ||||
| import androidx.work.WorkerParameters | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.main | ||||
| import com.lagradost.cloudstream3.utils.DataStore.getKey | ||||
| import com.lagradost.cloudstream3.utils.DataStore.removeKey | ||||
|  | @ -33,7 +34,6 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo | |||
|                 val info = applicationContext.getKey<VideoDownloadManager.DownloadInfo>(WORK_KEY_INFO, key) | ||||
|                 val pkg = | ||||
|                     applicationContext.getKey<VideoDownloadManager.DownloadResumePackage>(WORK_KEY_PACKAGE, key) | ||||
| 
 | ||||
|                 if (info != null) { | ||||
|                     downloadEpisode( | ||||
|                         applicationContext, | ||||
|  | @ -44,6 +44,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo | |||
|                         ::handleNotification | ||||
|                     ) | ||||
|                     awaitDownload(info.ep.id) | ||||
| 
 | ||||
|                 } else if (pkg != null) { | ||||
|                     downloadFromResume(applicationContext, pkg, ::handleNotification) | ||||
|                     awaitDownload(pkg.item.ep.id) | ||||
|  | @ -52,6 +53,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo | |||
|             } | ||||
|             return Result.success() | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             if (key != null) { | ||||
|                 removeKeys(key) | ||||
|             } | ||||
|  | @ -79,6 +81,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo | |||
|         } | ||||
|         downloadStatusEvent += listener | ||||
|         while (!isDone) { | ||||
|             println("AWAITING $id") | ||||
|             delay(1000) | ||||
|         } | ||||
|         downloadStatusEvent -= listener | ||||
|  |  | |||
|  | @ -15,11 +15,13 @@ import androidx.annotation.RequiresApi | |||
| import androidx.core.app.NotificationCompat | ||||
| import androidx.core.app.NotificationManagerCompat | ||||
| import androidx.core.net.toUri | ||||
| import androidx.preference.PreferenceManager | ||||
| import androidx.work.Data | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| import androidx.work.OneTimeWorkRequest | ||||
| import androidx.work.WorkManager | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
| import com.hippo.unifile.UniFile | ||||
| import com.lagradost.cloudstream3.MainActivity | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
|  | @ -33,7 +35,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | |||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.withContext | ||||
| import java.io.* | ||||
| import okhttp3.internal.closeQuietly | ||||
| import java.io.BufferedInputStream | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
| import java.io.OutputStream | ||||
| import java.lang.Thread.sleep | ||||
| import java.net.URI | ||||
| import java.net.URL | ||||
|  | @ -126,7 +132,8 @@ object VideoDownloadManager { | |||
|         val totalBytes: Long, | ||||
|         val relativePath: String, | ||||
|         val displayName: String, | ||||
|         val extraInfo: String? = null | ||||
|         val extraInfo: String? = null, | ||||
|         val basePath: String? = null // null is for legacy downloads. See getDefaultPath() | ||||
|     ) | ||||
| 
 | ||||
|     data class DownloadedFileInfoResult( | ||||
|  | @ -210,6 +217,7 @@ object VideoDownloadManager { | |||
|             } | ||||
|             return null | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
|  | @ -256,9 +264,9 @@ object VideoDownloadManager { | |||
|                 flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK | ||||
|             } | ||||
|             val pendingIntent: PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||||
|                 PendingIntent.getActivity(context, 0, intent,  PendingIntent.FLAG_IMMUTABLE) | ||||
|                 PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) | ||||
|             } else { | ||||
|                 PendingIntent.getActivity(context, 0, intent,  0) | ||||
|                 PendingIntent.getActivity(context, 0, intent, 0) | ||||
|             } | ||||
|             builder.setContentIntent(pendingIntent) | ||||
|         } | ||||
|  | @ -433,25 +441,34 @@ object VideoDownloadManager { | |||
|             } | ||||
|             return list | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun getFolder(context: Context, relativePath: String): List<Pair<String, Uri>>? { | ||||
|         if (isScopedStorage()) { | ||||
|     /** | ||||
|      * Used for getting video player subs. | ||||
|      * @return List of pairs for the files in this format: <Name, Uri> | ||||
|      * */ | ||||
|     fun getFolder(context: Context, relativePath: String, basePath: String?): List<Pair<String, Uri>>? { | ||||
|         val base = basePathToFile(context, basePath) | ||||
|         val folder = base?.gotoDir(relativePath, false) | ||||
| 
 | ||||
|         if (isScopedStorage && base.isDownloadDir()) { | ||||
|             return context.contentResolver?.getExistingFolderStartName(relativePath) | ||||
|         } else { | ||||
|             val normalPath = | ||||
|                 "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace( | ||||
|                     '/', | ||||
|                     File.separatorChar | ||||
|                 ) | ||||
|             val folder = File(normalPath) | ||||
|             if (folder.isDirectory) { | ||||
|                 return folder.listFiles()?.map { Pair(it.name, it.toUri()) } | ||||
| //            val normalPath = | ||||
| //                "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace( | ||||
| //                    '/', | ||||
| //                    File.separatorChar | ||||
| //                ) | ||||
| //            val folder = File(normalPath) | ||||
|             if (folder?.isDirectory == true) { | ||||
|                 return folder.listFiles()?.map { Pair(it.name ?: "", it.uri) } | ||||
|             } | ||||
|             return null | ||||
|         } | ||||
|         return null | ||||
| //        } | ||||
|     } | ||||
| 
 | ||||
|     @RequiresApi(Build.VERSION_CODES.Q) | ||||
|  | @ -487,6 +504,7 @@ object VideoDownloadManager { | |||
|             } | ||||
|             return null | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
|  | @ -497,13 +515,14 @@ object VideoDownloadManager { | |||
|             this.openFileDescriptor(fileUri, "r") | ||||
|                 .use { it?.statSize ?: 0 } | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun isScopedStorage(): Boolean { | ||||
|         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q | ||||
|     } | ||||
|     val isScopedStorage: Boolean | ||||
|         get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q | ||||
| 
 | ||||
| 
 | ||||
|     data class CreateNotificationMetadata( | ||||
|         val type: DownloadType, | ||||
|  | @ -518,6 +537,10 @@ object VideoDownloadManager { | |||
|         val fileStream: OutputStream? = null, | ||||
|     ) | ||||
| 
 | ||||
|     /** | ||||
|      * Sets up the appropriate file and creates a data stream from the file. | ||||
|      * Used for initializing downloads. | ||||
|      * */ | ||||
|     private fun setupStream( | ||||
|         context: Context, | ||||
|         name: String, | ||||
|  | @ -525,16 +548,17 @@ object VideoDownloadManager { | |||
|         extension: String, | ||||
|         tryResume: Boolean, | ||||
|     ): StreamData { | ||||
|         val relativePath = getRelativePath(folder) | ||||
|         val displayName = getDisplayName(name, extension) | ||||
|         val fileStream: OutputStream | ||||
|         val fileLength: Long | ||||
|         var resume = tryResume | ||||
|         if (isScopedStorage()) { | ||||
|         val baseFile = context.getBasePath() | ||||
| 
 | ||||
|         if (isScopedStorage && baseFile.first?.isDownloadDir() == true) { | ||||
|             val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) | ||||
| 
 | ||||
|             val currentExistingFile = | ||||
|                 cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH | ||||
|                 cr.getExistingDownloadUriOrNullQ(folder ?: "", displayName) // CURRENT FILE WITH THE SAME PATH | ||||
| 
 | ||||
|             fileLength = | ||||
|                 if (currentExistingFile == null || !resume) 0 else (cr.getFileLength(currentExistingFile) | ||||
|  | @ -566,7 +590,7 @@ object VideoDownloadManager { | |||
|                     put(MediaStore.MediaColumns.TITLE, name) | ||||
|                     if (currentMimeType != null) | ||||
|                         put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType) | ||||
|                     put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) | ||||
|                     put(MediaStore.MediaColumns.RELATIVE_PATH, folder) | ||||
|                 } | ||||
| 
 | ||||
|                 cr.insert( | ||||
|  | @ -578,26 +602,24 @@ object VideoDownloadManager { | |||
|             fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else "")) | ||||
|                 ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) | ||||
|         } else { | ||||
|             val normalPath = getNormalPath(relativePath, displayName) | ||||
|             // NORMAL NON SCOPED STORAGE FILE CREATION | ||||
|             val rFile = File(normalPath) | ||||
|             if (!rFile.exists()) { | ||||
|             val subDir = baseFile.first?.gotoDir(folder) | ||||
|             val rFile = subDir?.findFile(displayName) | ||||
|             if (rFile?.exists() != true) { | ||||
|                 fileLength = 0 | ||||
|                 rFile.parentFile?.mkdirs() | ||||
|                 if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) | ||||
|                 if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) | ||||
|             } else { | ||||
|                 if (resume) { | ||||
|                     fileLength = rFile.length() | ||||
|                     fileLength = rFile.size() | ||||
|                 } else { | ||||
|                     fileLength = 0 | ||||
|                     rFile.parentFile?.mkdirs() | ||||
|                     if (!rFile.delete()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) | ||||
|                     if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) | ||||
|                     if (!rFile.delete()) return StreamData(ERROR_DELETING_FILE) | ||||
|                     if (subDir.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) | ||||
|                 } | ||||
|             } | ||||
|             fileStream = FileOutputStream(rFile, false) | ||||
|             fileStream = (subDir.findFile(displayName) ?: subDir.createFile(displayName))!!.openOutputStream() | ||||
| //          fileStream = FileOutputStream(rFile, false) | ||||
|             if (fileLength == 0L) resume = false | ||||
|         } | ||||
|         if (fileLength == 0L) resume = false | ||||
|         return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) | ||||
|     } | ||||
| 
 | ||||
|  | @ -615,15 +637,16 @@ object VideoDownloadManager { | |||
|             return ERROR_UNKNOWN | ||||
|         } | ||||
| 
 | ||||
|         val relativePath = getRelativePath(folder) | ||||
|         val displayName = getDisplayName(name, extension) | ||||
|         val basePath = context.getBasePath() | ||||
| 
 | ||||
|         val displayName = getDisplayName(name, extension) | ||||
|         val relativePath = if (isScopedStorage && basePath.first.isDownloadDir()) getRelativePath(folder) else folder | ||||
| 
 | ||||
|         fun deleteFile(): Int { | ||||
|             return delete(context, name, folder, extension, parentId) | ||||
|             return delete(context, name, relativePath, extension, parentId, basePath.first) | ||||
|         } | ||||
| 
 | ||||
|         val stream = setupStream(context, name, folder, extension, tryResume) | ||||
|         val stream = setupStream(context, name, relativePath, extension, tryResume) | ||||
|         if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode | ||||
| 
 | ||||
|         val resume = stream.resume!! | ||||
|  | @ -681,7 +704,12 @@ object VideoDownloadManager { | |||
|             context.setKey( | ||||
|                 KEY_DOWNLOAD_INFO, | ||||
|                 it.toString(), | ||||
|                 DownloadedFileInfo(bytesTotal, relativePath, displayName) | ||||
|                 DownloadedFileInfo( | ||||
|                     bytesTotal, | ||||
|                     relativePath ?: "", | ||||
|                     displayName, | ||||
|                     basePath = basePath.second | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  | @ -789,6 +817,7 @@ object VideoDownloadManager { | |||
|                 fileStream.write(buffer, 0, count) | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             isFailed = true | ||||
|             updateNotification() | ||||
|         } | ||||
|  | @ -832,16 +861,118 @@ object VideoDownloadManager { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getRelativePath(folder: String?): String { | ||||
|         return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar) | ||||
| 
 | ||||
|     /** | ||||
|      * Guarantees a directory is present with the dir name (if createMissingDirectories is true). | ||||
|      * Works recursively when '/' is present. | ||||
|      * Will remove any file with the dir name if present and add directory. | ||||
|      * Will not work if the parent directory does not exist. | ||||
|      * | ||||
|      * @param directoryName if null will use the current path. | ||||
|      * @return UniFile / null if createMissingDirectories = false and folder is not found. | ||||
|      * */ | ||||
|     private fun UniFile.gotoDir(directoryName: String?, createMissingDirectories: Boolean = true): UniFile? { | ||||
| 
 | ||||
|         // May give this error on scoped storage. | ||||
|         // W/DocumentsContract: Failed to create document | ||||
|         // java.lang.IllegalArgumentException: Parent document isn't a directory | ||||
| 
 | ||||
|         // Not present in latest testing. | ||||
| 
 | ||||
| //        println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") | ||||
| 
 | ||||
|         try { | ||||
|             // Creates itself from parent if doesn't exist. | ||||
|             if (!this.exists() && createMissingDirectories && !this.name.isNullOrBlank()) { | ||||
|                 if (this.parentFile != null) { | ||||
|                     this.parentFile?.createDirectory(this.name) | ||||
|                 } else if (this.filePath != null) { | ||||
|                     UniFile.fromFile(File(this.filePath!!).parentFile)?.createDirectory(this.name) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             val allDirectories = directoryName?.split("/") | ||||
|             return if (allDirectories?.size == 1 || allDirectories == null) { | ||||
|                 val found = this.findFile(directoryName) | ||||
|                 when { | ||||
|                     directoryName.isNullOrBlank() -> this | ||||
|                     found?.isDirectory == true -> found | ||||
| 
 | ||||
|                     !createMissingDirectories -> null | ||||
|                     // Below creates directories | ||||
|                     found?.isFile == true -> { | ||||
|                         found.delete() | ||||
|                         this.createDirectory(directoryName) | ||||
|                     } | ||||
|                     this.isDirectory -> this.createDirectory(directoryName) | ||||
|                     else -> this.parentFile?.createDirectory(directoryName) | ||||
|                 } | ||||
|             } else { | ||||
|                 var currentDirectory = this | ||||
|                 allDirectories.forEach { | ||||
|                     // If the next directory is not found it returns the deepest directory possible. | ||||
|                     val nextDir = currentDirectory.gotoDir(it, createMissingDirectories) | ||||
|                     currentDirectory = nextDir ?: return null | ||||
|                 } | ||||
|                 currentDirectory | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getDisplayName(name: String, extension: String): String { | ||||
|         return "$name.$extension" | ||||
|     } | ||||
| 
 | ||||
|     private fun getNormalPath(relativePath: String, displayName: String): String { | ||||
|         return "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName" | ||||
|     /** | ||||
|      * Gets the default download path as an UniFile. | ||||
|      * Vital for legacy downloads, be careful about changing anything here. | ||||
|      * | ||||
|      * As of writing UniFile is used for everything but download directory on scoped storage. | ||||
|      * Special ContentResolver fuckery is needed for that as UniFile doesn't work. | ||||
|      * */ | ||||
|     fun getDownloadDir(): UniFile? { | ||||
|         // See https://www.py4u.net/discuss/614761 | ||||
|         return UniFile.fromFile( | ||||
|             File( | ||||
|                 Environment.getExternalStorageDirectory().absolutePath + File.separatorChar + | ||||
|                         Environment.DIRECTORY_DOWNLOADS | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Deprecated("TODO fix UniFile to work with download directory.") | ||||
|     private fun getRelativePath(folder: String?): String { | ||||
|         return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Turns a string to an UniFile. Used for stored string paths such as settings. | ||||
|      * Should only be used to get a download path. | ||||
|      * */ | ||||
|     private fun basePathToFile(context: Context, path: String?): UniFile? { | ||||
|         return when { | ||||
|             path.isNullOrBlank() -> getDownloadDir() | ||||
|             path.startsWith("content://") -> UniFile.fromUri(context, path.toUri()) | ||||
|             else -> UniFile.fromFile(File(path)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Base path where downloaded things should be stored, changes depending on settings. | ||||
|      * Returns the file and a string to be stored for future file retrieval. | ||||
|      * UniFile.filePath is not sufficient for storage. | ||||
|      * */ | ||||
|     fun Context.getBasePath(): Pair<UniFile?, String?> { | ||||
|         val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) | ||||
|         val basePathSetting = settingsManager.getString(getString(R.string.download_path_key), null) | ||||
|         return basePathToFile(this, basePathSetting) to basePathSetting | ||||
|     } | ||||
| 
 | ||||
|     private fun UniFile?.isDownloadDir(): Boolean { | ||||
|         return this != null && this.filePath == getDownloadDir()?.filePath | ||||
|     } | ||||
| 
 | ||||
|     private fun delete( | ||||
|  | @ -850,20 +981,29 @@ object VideoDownloadManager { | |||
|         folder: String?, | ||||
|         extension: String, | ||||
|         parentId: Int?, | ||||
|         basePath: UniFile? | ||||
|     ): Int { | ||||
|         val relativePath = getRelativePath(folder) | ||||
|         val displayName = getDisplayName(name, extension) | ||||
| 
 | ||||
|         if (isScopedStorage()) { | ||||
|         // If scoped storage and using download dir (not accessible with UniFile) | ||||
|         if (isScopedStorage && basePath.isDownloadDir()) { | ||||
|             val relativePath = getRelativePath(folder) | ||||
|             val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) | ||||
|             if (lastContent != null) { | ||||
|                 context.contentResolver.delete(lastContent, null, null) | ||||
|             } | ||||
|         } else { | ||||
|             if (!File(getNormalPath(relativePath, displayName)).delete()) return ERROR_DELETING_FILE | ||||
|         } | ||||
|         parentId?.let { | ||||
|             downloadDeleteEvent.invoke(parentId) | ||||
|             val dir = basePath?.gotoDir(folder) | ||||
|             val file = dir?.findFile(displayName) | ||||
|             val success = file?.delete() | ||||
|             if (success != true) return ERROR_DELETING_FILE else { | ||||
|                 // Cleans up empty directory | ||||
|                 if (dir.listFiles()?.isEmpty() == true) dir.delete() | ||||
|             } | ||||
| //        } | ||||
|             parentId?.let { | ||||
|                 downloadDeleteEvent.invoke(parentId) | ||||
|             } | ||||
|         } | ||||
|         return SUCCESS_STOPPED | ||||
|     } | ||||
|  | @ -898,16 +1038,20 @@ object VideoDownloadManager { | |||
|         ) | ||||
| 
 | ||||
|         var realIndex = startIndex ?: 0 | ||||
|         val stream = setupStream(context, name, folder, extension, realIndex > 0) | ||||
|         val basePath = context.getBasePath() | ||||
| 
 | ||||
|         val relativePath = if (isScopedStorage && basePath.first.isDownloadDir()) getRelativePath(folder) else folder | ||||
| 
 | ||||
|         val stream = setupStream(context, name, relativePath, extension, realIndex > 0) | ||||
|         if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode | ||||
| 
 | ||||
|         if (!stream.resume!!) realIndex = 0 | ||||
|         val fileLengthAdd = stream.fileLength!! | ||||
|         val tsIterator = m3u8Helper.hlsYield(listOf(m3u8), realIndex) | ||||
| 
 | ||||
|         val relativePath = getRelativePath(folder) | ||||
|         val displayName = getDisplayName(name, extension) | ||||
| 
 | ||||
| 
 | ||||
|         val fileStream = stream.fileStream!! | ||||
| 
 | ||||
|         val firstTs = tsIterator.next() | ||||
|  | @ -920,7 +1064,7 @@ object VideoDownloadManager { | |||
|         val totalTs = firstTs.totalTs.toLong() | ||||
| 
 | ||||
|         fun deleteFile(): Int { | ||||
|             return delete(context, name, folder, extension, parentId) | ||||
|             return delete(context, name, relativePath, extension, parentId, basePath.first) | ||||
|         } | ||||
|         /* | ||||
|             Most of the auto generated m3u8 out there have TS of the same size. | ||||
|  | @ -939,13 +1083,15 @@ object VideoDownloadManager { | |||
|                     it.toString(), | ||||
|                     DownloadedFileInfo( | ||||
|                         (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), | ||||
|                         relativePath, | ||||
|                         relativePath ?: "", | ||||
|                         displayName, | ||||
|                         tsProgress.toString() | ||||
|                         tsProgress.toString(), | ||||
|                         basePath = basePath.second | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         updateInfo() | ||||
| 
 | ||||
|         fun updateNotification() { | ||||
|  | @ -1153,7 +1299,6 @@ object VideoDownloadManager { | |||
|                     ) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } ?: ERROR_UNKNOWN | ||||
|     } | ||||
| 
 | ||||
|  | @ -1189,7 +1334,7 @@ object VideoDownloadManager { | |||
|                                     link, | ||||
|                                     notificationCallback, | ||||
|                                     resume | ||||
|                                 ) | ||||
|                                 ).also { println("Single episode finished with return code: $it") } | ||||
|                             } | ||||
|                         } | ||||
|                         if (connectionResult != null && connectionResult > 0) { // SUCCESS | ||||
|  | @ -1217,8 +1362,9 @@ object VideoDownloadManager { | |||
| 
 | ||||
|     private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? { | ||||
|         val info = context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null | ||||
|         val base = basePathToFile(context, info.basePath) | ||||
| 
 | ||||
|         if (isScopedStorage()) { | ||||
|         if (isScopedStorage && base.isDownloadDir()) { | ||||
|             val cr = context.contentResolver ?: return null | ||||
|             val fileUri = | ||||
|                 cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) ?: return null | ||||
|  | @ -1226,10 +1372,28 @@ object VideoDownloadManager { | |||
|             if (fileLength == 0L) return null | ||||
|             return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri) | ||||
|         } else { | ||||
|             val normalPath = getNormalPath(info.relativePath, info.displayName) | ||||
|             val dFile = File(normalPath) | ||||
|             if (!dFile.exists()) return null | ||||
|             return DownloadedFileInfoResult(dFile.length(), info.totalBytes, dFile.toUri()) | ||||
| 
 | ||||
|             val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) | ||||
| 
 | ||||
| //            val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) | ||||
| //            val dFile = File(normalPath) | ||||
| 
 | ||||
|             if (file?.exists() != true) return null | ||||
| 
 | ||||
|             return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the true download size as Scoped Storage sometimes wrongly returns 0. | ||||
|      * */ | ||||
|     fun UniFile.size(): Long { | ||||
|         val len = length() | ||||
|         return if (len <= 1) { | ||||
|             val inputStream = this.openInputStream() | ||||
|             return inputStream.available().toLong().also { inputStream.closeQuietly() } | ||||
|         } else { | ||||
|             len | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -1245,8 +1409,8 @@ object VideoDownloadManager { | |||
|         downloadProgressEvent.invoke(Triple(id, 0, 0)) | ||||
|         downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped)) | ||||
|         downloadDeleteEvent.invoke(id) | ||||
| 
 | ||||
|         if (isScopedStorage()) { | ||||
|         val base = basePathToFile(context, info.basePath) | ||||
|         if (isScopedStorage && base.isDownloadDir()) { | ||||
|             val cr = context.contentResolver ?: return false | ||||
|             val fileUri = | ||||
|                 cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) | ||||
|  | @ -1254,10 +1418,17 @@ object VideoDownloadManager { | |||
| 
 | ||||
|             return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 | ||||
|         } else { | ||||
|             val normalPath = getNormalPath(info.relativePath, info.displayName) | ||||
|             val dFile = File(normalPath) | ||||
|             if (!dFile.exists()) return true | ||||
|             return dFile.delete() | ||||
|             val file = base?.gotoDir(info.relativePath)?.findFile(info.displayName) | ||||
| //            val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) | ||||
| //            val dFile = File(normalPath) | ||||
|             if (file?.exists() != true) return true | ||||
|             return try { | ||||
|                 file.delete() | ||||
|             } catch (e: Exception) { | ||||
|                 logError(e) | ||||
|                 val cr = context.contentResolver | ||||
|                 cr.delete(file.uri, null, null) > 0 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -1272,19 +1443,19 @@ object VideoDownloadManager { | |||
|         setKey: Boolean = true | ||||
|     ) { | ||||
|         if (!currentDownloads.any { it == pkg.item.ep.id }) { | ||||
|             if (currentDownloads.size == maxConcurrentDownloads) { | ||||
|                 main { | ||||
| //                    showToast( // can be replaced with regular Toast | ||||
| //                        context, | ||||
| //                        "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${ | ||||
| //                            context.getString( | ||||
| //                                R.string.queued | ||||
| //                            ) | ||||
| //                        }", | ||||
| //                        Toast.LENGTH_SHORT | ||||
| //                    ) | ||||
|                 } | ||||
|             } | ||||
| //            if (currentDownloads.size == maxConcurrentDownloads) { | ||||
| //                main { | ||||
| ////                    showToast( // can be replaced with regular Toast | ||||
| ////                        context, | ||||
| ////                        "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${ | ||||
| ////                            context.getString( | ||||
| ////                                R.string.queued | ||||
| ////                            ) | ||||
| ////                        }", | ||||
| ////                        Toast.LENGTH_SHORT | ||||
| ////                    ) | ||||
| //                } | ||||
| //            } | ||||
|             downloadQueue.addLast(pkg) | ||||
|             downloadCheck(context, notificationCallback) | ||||
|             if (setKey) saveQueue(context) | ||||
|  |  | |||
|  | @ -24,11 +24,14 @@ | |||
|     <string name="show_fillers_key" translatable="false">show_fillers_key</string> | ||||
|     <string name="provider_lang_key" translatable="false">provider_lang_key</string> | ||||
|     <string name="dns_key" translatable="false">dns_key</string> | ||||
|     <string name="download_path_key" translatable="false">download_path_key</string> | ||||
|     <string name="app_name_download_path" translatable="false">Cloudstream</string> | ||||
|     <string name="app_layout_key" translatable="false">app_layout_key</string> | ||||
|     <string name="primary_color_key" translatable="false">primary_color_key</string> | ||||
|     <string name="prefer_media_type_key" translatable="false">prefer_media_type_key</string> | ||||
|     <string name="app_theme_key" translatable="false">app_theme_key</string> | ||||
| 
 | ||||
| 
 | ||||
|     <!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG --> | ||||
|     <string name="extra_info_format" translatable="false" formatted="true">%d %s | %sMB</string> | ||||
|     <string name="storage_size_format" translatable="false" formatted="true">%s • %sGB</string> | ||||
|  | @ -270,6 +273,7 @@ | |||
|     <string name="dns_pref">DNS over HTTPS</string> | ||||
|     <string name="dns_pref_summary">Useful for bypassing ISP blocks</string> | ||||
| 
 | ||||
|     <string name="download_path_pref">Download path</string> | ||||
| 
 | ||||
|     <string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string> | ||||
| 
 | ||||
|  |  | |||
|  | @ -104,6 +104,11 @@ | |||
|                 android:summary="@string/dns_pref_summary" | ||||
|                 android:icon="@drawable/ic_baseline_dns_24"> | ||||
|         </Preference> | ||||
|         <Preference | ||||
|                 android:key="@string/download_path_key" | ||||
|                 android:title="@string/download_path_pref" | ||||
|                 android:icon="@drawable/netflix_download"> | ||||
|         </Preference> | ||||
|         <Preference | ||||
|                 android:icon="@drawable/ic_baseline_tv_24" | ||||
|                 android:key="@string/app_layout_key" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue