mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
9ee3f1f26e
12 changed files with 72 additions and 51 deletions
|
@ -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") };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue