Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2023-11-13 22:10:43 +01:00
commit 9ee3f1f26e
12 changed files with 72 additions and 51 deletions

View file

@ -519,6 +519,13 @@ class Settings : FragmentedStorageFileJson() {
StateApp.instance.changeExternalDownloadDirectory(it);
}
}
@FormField(R.string.clear_external_downloads_directory, FieldForm.BUTTON, R.string.clear_the_external_storage_for_download_files, 5)
fun clearStorageDownload() {
Settings.instance.storage.storage_download = null;
Settings.instance.save();
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
}
}

View file

@ -164,9 +164,7 @@ fun Int.sp(resources: Resources): Int {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), resources.displayMetrics).toInt()
}
fun File.share(context: Context) {
val uri = FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), this);
fun DocumentFile.share(context: Context) {
val shareIntent = Intent();
shareIntent.action = Intent.ACTION_SEND;
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

View file

@ -1,13 +1,18 @@
package com.futo.platformplayer.downloads
import android.content.Context
import android.net.Uri
import android.os.Environment
import androidx.documentfile.provider.DocumentFile
import com.arthenica.ffmpegkit.*
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanBitrate
import kotlinx.coroutines.*
import java.io.*
import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
@ -43,7 +48,7 @@ class VideoExport {
this.subtitleSource = subtitleSource;
}
suspend fun export(onProgress: ((Double) -> Unit)? = null): File = coroutineScope {
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
if(isCancelled) throw CancellationException("Export got cancelled");
val v = videoSource;
@ -55,34 +60,47 @@ class VideoExport {
if (a != null) sourceCount++;
if (s != null) sourceCount++;
var outputFile: File? = null;
val moviesRoot = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
val musicRoot = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
val moviesGrayjay = File(moviesRoot, "Grayjay");
val musicGrayjay = File(musicRoot, "Grayjay");
if(!moviesGrayjay.exists())
moviesGrayjay.mkdirs();
if(!musicGrayjay.exists())
musicGrayjay.mkdirs();
val outputFile: DocumentFile?;
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
if (sourceCount > 1) {
val outputFileName = toSafeFileName(videoLocal.name) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
val f = File(moviesGrayjay, outputFileName);
val f = downloadRoot.createFile("video/mp4", outputFileName)
?: throw Exception("Failed to create file in external directory.");
Logger.i(TAG, "Combining video and audio through FFMPEG.");
combine(a?.filePath, v?.filePath, s?.filePath, f.absolutePath, videoLocal.duration.toDouble()) { progress -> onProgress?.invoke(progress) };
val tempFile = File(context.cacheDir, "${UUID.randomUUID()}.mp4");
try {
combine(a?.filePath, v?.filePath, s?.filePath, tempFile.absolutePath, videoLocal.duration.toDouble()) { progress -> onProgress?.invoke(progress) };
context.contentResolver.openOutputStream(f.uri)?.use { outputStream ->
copy(tempFile.absolutePath, outputStream) { progress -> onProgress?.invoke(progress) };
}
} finally {
tempFile.delete();
}
outputFile = f;
} else if (v != null) {
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.videoContainerToExtension(v.container);
val f = File(moviesGrayjay, outputFileName);
val f = downloadRoot.createFile(v.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
Logger.i(TAG, "Copying video.");
copy(v.filePath, f.absolutePath) { progress -> onProgress?.invoke(progress) };
context.contentResolver.openOutputStream(f.uri)?.use { outputStream ->
copy(v.filePath, outputStream) { progress -> onProgress?.invoke(progress) };
}
outputFile = f;
} else if (a != null) {
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.audioContainerToExtension(a.container);
val f = File(musicGrayjay, outputFileName);
val f = downloadRoot.createFile(a.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
Logger.i(TAG, "Copying audio.");
copy(a.filePath, f.absolutePath) { progress -> onProgress?.invoke(progress) };
context.contentResolver.openOutputStream(f.uri)?.use { outputStream ->
copy(a.filePath, outputStream) { progress -> onProgress?.invoke(progress) };
}
outputFile = f;
} else {
throw Exception("Cannot export when no audio or video source is set.");
@ -179,10 +197,9 @@ class VideoExport {
}
}
private suspend fun copy(fromPath: String, toPath: String, bufferSize: Int = 8192, onProgress: ((Double) -> Unit)? = null) {
private suspend fun copy(fromPath: String, outputStream: OutputStream, bufferSize: Int = 8192, onProgress: ((Double) -> Unit)? = null) {
withContext(Dispatchers.IO) {
var inputStream: FileInputStream? = null
var outputStream: FileOutputStream? = null
try {
val srcFile = File(fromPath)
@ -190,17 +207,7 @@ class VideoExport {
throw IOException("Source file not found.")
}
val dstFile = File(toPath)
val parentDir = dstFile.parentFile ?: throw IOException("Non existent parent dir.")
if (!parentDir.exists()) {
if (!parentDir.mkdirs()) {
throw IOException("Failed to create destination directory.")
}
}
inputStream = FileInputStream(srcFile)
outputStream = FileOutputStream(dstFile)
val buffer = ByteArray(bufferSize)
val totalBytes = srcFile.length()
@ -221,7 +228,6 @@ class VideoExport {
throw IOException("Error occurred while copying file: ${e.message}", e)
} finally {
inputStream?.close()
outputStream?.close()
}
}
}

View file

@ -91,7 +91,7 @@ class BuyFragment : MainFragment() {
val price = prices[currency.id]!!;
val priceDecimal = (price.toDouble() / 100);
withContext(Dispatchers.Main) {
_buttonBuyText.text = currency.symbol + String.format("%.2f", priceDecimal);
_buttonBuyText.text = currency.symbol + String.format("%.2f", priceDecimal) + context.getString(R.string.plus_tax);
}
}
}

View file

@ -107,7 +107,7 @@ class ExportingService : Service() {
{
try{
notifyExport(currentExport);
doExport(currentExport);
doExport(applicationContext, currentExport);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex);
@ -125,13 +125,13 @@ class ExportingService : Service() {
stopService(this);
}
private suspend fun doExport(export: VideoExport) {
private suspend fun doExport(context: Context, export: VideoExport) {
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
export.changeState(VideoExport.State.EXPORTING);
var lastNotifyTime: Long = 0L;
val file = export.export { progress ->
val file = export.export(context) { progress ->
export.progress = progress;
val currentTime = System.currentTimeMillis();
@ -146,7 +146,7 @@ class ExportingService : Service() {
notifyExport(export);
withContext(Dispatchers.Main) {
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.path}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
file.share(this@ExportingService);
};
}

View file

@ -111,17 +111,13 @@ class StateApp {
return null;
}
fun changeExternalDownloadDirectory(context: IWithResultLauncher, onChanged: ((DocumentFile?)->Unit)? = null) {
scopeOrNull?.launch(Dispatchers.Main) {
UIDialogs.toast("External download directory not yet used by export (WIP)");
};
if(context is Context)
requestDirectoryAccess(context, "Download Exports", "This directory is used to export downloads to for external usage.", null) {
if(it != null)
context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION));
if(it != null && isValidStorageUri(context, it)) {
Logger.i(TAG, "Changed external download directory: ${it}");
Settings.instance.storage.storage_general = it.toString();
Settings.instance.storage.storage_download = it.toString();
Settings.instance.save();
onChanged?.invoke(getExternalDownloadDirectory(context));

View file

@ -400,10 +400,7 @@ class StateDownloads {
_exporting.save(videoExport);
if(notify) {
if(videoSource == null)
UIDialogs.toast("Exporting [${shortName}]\nIn your music directory under Grayjay");
else
UIDialogs.toast("Exporting [${shortName}]\nIn your movies directory under Grayjay");
UIDialogs.toast("Exporting [${shortName}]");
StateApp.withContext { ExportingService.getOrCreateService(it) };
onExportsChanged.emit();
}

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.app.Activity
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageButton
@ -7,9 +8,11 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.adapters.AnyAdapter
@ -47,7 +50,18 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<
}
_videoExport.setOnClickListener {
val v = _video ?: return@setOnClickListener;
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
if (StateApp.instance.getExternalDownloadDirectory(_view.context) == null) {
StateApp.instance.changeExternalDownloadDirectory(_view.context as MainActivity) {
if (it == null) {
UIDialogs.toast(_view.context, "Download directory must be set to export.");
return@changeExternalDownloadDirectory;
}
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
};
} else {
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
}
}
}

View file

@ -76,7 +76,7 @@
android:id="@+id/button_buy_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="$9.99"
android:text="$9.99 + Tax"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"

View file

@ -278,9 +278,11 @@
<string name="casting">Casting</string>
<string name="change_behavior_of_the_player">Change behavior of the player</string>
<string name="change_external_downloads_directory">Change external Downloads directory</string>
<string name="clear_external_downloads_directory">Clear external Downloads directory</string>
<string name="change_external_general_directory">Change external General directory</string>
<string name="change_tabs_visible_on_the_home_screen">Change tabs visible on the home screen</string>
<string name="change_the_external_directory_for_general_files">Change the external directory for general files</string>
<string name="clear_the_external_storage_for_download_files">Clear the external storage for download files</string>
<string name="change_the_external_storage_for_download_files">Change the external storage for download files</string>
<string name="clear_cookies">Clear Cookies</string>
<string name="clear_cookies_on_logout">Clear Cookies on Logout</string>
@ -654,6 +656,7 @@
<string name="load_more">Load More</string>
<string name="stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more">Stopped after {requestCount} to avoid rate limit, click load more to load more.</string>
<string name="this_creator_has_not_setup_any_monetization_features">This creator has not setup any monetization features</string>
<string name="plus_tax">" + Tax"</string>
<string-array name="home_screen_array">
<item>Recommendations</item>
<item>Subscriptions</item>

@ -1 +1 @@
Subproject commit 3aa9acaefe495e388b9d7542e246f8e24321069c
Subproject commit 60a7ee2ddf71b936d9c289a3343020cc20edfe56

@ -1 +1 @@
Subproject commit 3aa9acaefe495e388b9d7542e246f8e24321069c
Subproject commit 60a7ee2ddf71b936d9c289a3343020cc20edfe56