made file transfer a separate worker

made the app create default folders since downloading on a fresh install wouldnt work without checking the settings first, now you can
made each download have their own separate temporary folder using an url and download type. this way they can be resumed
fixed sponsorblock filters not working on a fresh install
fixed custom command notification not being in the proper channel
and other small stuff
This commit is contained in:
Denis Çerri 2023-01-08 00:32:46 +01:00
parent 6a1ed4fc7e
commit 51ff22abcd
No known key found for this signature in database
GPG key ID: 96B3554AF5B193EE
16 changed files with 295 additions and 68 deletions

View file

@ -119,7 +119,7 @@ dependencies {
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'com.facebook.shimmer:shimmer:0.5.0' implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation "androidx.work:work-runtime:$workVer" implementation "androidx.work:work-runtime-ktx:$workVer"
implementation "androidx.room:room-runtime:2.4.3" implementation "androidx.room:room-runtime:2.4.3"
implementation "androidx.room:room-ktx:2.4.3" implementation "androidx.room:room-ktx:2.4.3"
@ -127,7 +127,8 @@ dependencies {
androidTestImplementation "androidx.room:room-testing:2.4.3" androidTestImplementation "androidx.room:room-testing:2.4.3"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation "androidx.compose.runtime:runtime:$composeVer" implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation "com.google.truth:truth:1.1.3" androidTestImplementation "com.google.truth:truth:1.1.3"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer"
} }

View file

@ -22,7 +22,6 @@
android:theme="@style/AppTheme" > android:theme="@style/AppTheme" >
<activity android:name=".MainActivity" <activity android:name=".MainActivity"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:label="@string/download"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -2,10 +2,15 @@ package com.deniscerri.ytdlnis
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.yausername.aria2c.Aria2c import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg import com.yausername.ffmpeg.FFmpeg
@ -17,6 +22,7 @@ import io.reactivex.exceptions.UndeliverableException
import io.reactivex.observers.DisposableCompletableObserver import io.reactivex.observers.DisposableCompletableObserver
import io.reactivex.plugins.RxJavaPlugins import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import java.io.File
class App : Application() { class App : Application() {
override fun onCreate() { override fun onCreate() {

View file

@ -7,10 +7,17 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.SystemClock
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.Settings.Global
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.database.Video import com.deniscerri.ytdlnis.database.Video
import com.deniscerri.ytdlnis.page.CustomCommandActivity import com.deniscerri.ytdlnis.page.CustomCommandActivity
import com.deniscerri.ytdlnis.service.DownloadInfo import com.deniscerri.ytdlnis.service.DownloadInfo
@ -18,6 +25,10 @@ import com.deniscerri.ytdlnis.service.IDownloaderListener
import com.deniscerri.ytdlnis.service.IDownloaderService import com.deniscerri.ytdlnis.service.IDownloaderService
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.work.FileTransferWorker
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.originDir
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.title
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.downLocation
import com.yausername.youtubedl_android.DownloadProgressCallback import com.yausername.youtubedl_android.DownloadProgressCallback
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
@ -26,6 +37,7 @@ import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.* import java.util.*
@ -46,6 +58,8 @@ class DownloaderService : Service() {
private var context: Context? = null private var context: Context? = null
var downloadProcessID = "processID" var downloadProcessID = "processID"
private var downloadNotificationID = 0 private var downloadNotificationID = 0
private lateinit var notificationChannelID : String
private lateinit var workManager : WorkManager
private val callback: (Float, Long?, String?) -> Unit = private val callback: (Float, Long?, String?) -> Unit =
{ progress: Float, _: Long?, line: String? -> { progress: Float, _: Long?, line: String? ->
@ -58,7 +72,8 @@ class DownloaderService : Service() {
} }
notificationUtil.updateDownloadNotification( notificationUtil.updateDownloadNotification(
downloadNotificationID, downloadNotificationID,
line!!, progress.toInt(), downloadQueue.size, title line!!, progress.toInt(), downloadQueue.size, title,
notificationChannelID
) )
try { try {
for (activity in activities.keys) { for (activity in activities.keys) {
@ -74,17 +89,11 @@ class DownloaderService : Service() {
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
} }
// private val callback = object : DownloadProgressCallback {
// override fun onProgressUpdate(progress: Float, etaInSeconds: Long, line: String?) {
// override fun onProgressUpdate(progress: Float, etaInSeconds: Long, line: String?) {
//
// }
// }
// }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
context = this context = this
this.workManager = WorkManager.getInstance(context!!.applicationContext)
} }
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
@ -93,9 +102,9 @@ class DownloaderService : Service() {
if (intent.getBooleanExtra("rebind", false)) { if (intent.getBooleanExtra("rebind", false)) {
return binder return binder
} }
val id = intent.getIntExtra("id", 1) when (val id = intent.getIntExtra("id", 1)) {
when (id) {
NotificationUtil.DOWNLOAD_NOTIFICATION_ID -> { NotificationUtil.DOWNLOAD_NOTIFICATION_ID -> {
notificationChannelID = NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
theIntent = Intent(this, MainActivity::class.java) theIntent = Intent(this, MainActivity::class.java)
pendingIntent = pendingIntent =
PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE)
@ -106,21 +115,23 @@ class DownloaderService : Service() {
downloadInfo.downloadQueue = downloadQueue downloadInfo.downloadQueue = downloadQueue
val title = downloadInfo.video.title val title = downloadInfo.video.title
val notification = val notification =
App.notificationUtil.createDownloadServiceNotification(pendingIntent, title) App.notificationUtil.createDownloadServiceNotification(pendingIntent, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
startForeground(downloadNotificationID, notification) startForeground(downloadNotificationID, notification)
startDownload(downloadQueue) startDownload(downloadQueue)
} }
NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID -> { NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID -> {
notificationChannelID = NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
theIntent = Intent(this, CustomCommandActivity::class.java) theIntent = Intent(this, CustomCommandActivity::class.java)
pendingIntent = pendingIntent =
PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE)
downloadNotificationID = id downloadNotificationID = id
val command = intent.getStringExtra("command") val command = intent.getStringExtra("command")
val command_notification = App.notificationUtil.createDownloadServiceNotification( val commandNotification = App.notificationUtil.createDownloadServiceNotification(
pendingIntent, pendingIntent,
getString(R.string.running_ytdlp_command) getString(R.string.running_ytdlp_command),
NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
) )
startForeground(downloadNotificationID, command_notification) startForeground(downloadNotificationID, commandNotification)
startCommandDownload(command) startCommandDownload(command)
} }
} }
@ -129,7 +140,11 @@ class DownloaderService : Service() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
stopForeground(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
stopForeground(STOP_FOREGROUND_REMOVE)
}else{
stopForeground(true)
}
stopSelf() stopSelf()
} }
@ -294,6 +309,11 @@ class DownloaderService : Service() {
val url = video.getURL() val url = video.getURL()
val request = YoutubeDLRequest(url) val request = YoutubeDLRequest(url)
val type = video.downloadedType val type = video.downloadedType
val downloadLocation = getDownloadLocation(type)
var tempFileDir = File(cacheDir.absolutePath + """/${video.title}##${video.downloadedType}""")
tempFileDir.delete()
tempFileDir.mkdir()
val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE) val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE)
val aria2 = sharedPreferences.getBoolean("aria2", false) val aria2 = sharedPreferences.getBoolean("aria2", false)
@ -312,11 +332,16 @@ class DownloaderService : Service() {
request.addOption("--convert-thumbnails", "png") request.addOption("--convert-thumbnails", "png")
} }
request.addOption("--no-mtime") request.addOption("--no-mtime")
val sponsorBlockFilters = sharedPreferences.getString("sponsorblock_filter", "") val sponsorBlockFilters = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
if (!sponsorBlockFilters!!.isEmpty()) { if (sponsorBlockFilters!!.isNotEmpty()) {
request.addOption("--sponsorblock-remove", sponsorBlockFilters) val filters = java.lang.String.join(",", sponsorBlockFilters)
request.addOption("--sponsorblock-remove", filters)
} }
if (type == "audio") { if (type == "audio") {
if (downloadLocation.equals(getString(R.string.music_path))){
tempFileDir = File(getString(R.string.music_path))
}
request.addOption("-x") request.addOption("-x")
var format = video.audioFormat var format = video.audioFormat
if (format == null) format = sharedPreferences.getString("audio_format", "") if (format == null) format = sharedPreferences.getString("audio_format", "")
@ -348,8 +373,12 @@ class DownloaderService : Service() {
video.author video.author
) )
) )
request.addOption("-o", filesDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s") request.addOption("-o", tempFileDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s")
} else if (type == "video") { } else if (type == "video") {
if (downloadLocation.equals(getString(R.string.video_path))){
tempFileDir = File(getString(R.string.video_path))
}
val addChapters = sharedPreferences.getBoolean("add_chapters", false) val addChapters = sharedPreferences.getBoolean("add_chapters", false)
if (addChapters) { if (addChapters) {
request.addOption("--sponsorblock-mark", "all") request.addOption("--sponsorblock-mark", "all")
@ -380,7 +409,7 @@ class DownloaderService : Service() {
request.addOption("--embed-thumbnail") request.addOption("--embed-thumbnail")
} }
} }
request.addOption("-o", filesDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s") request.addOption("-o", tempFileDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s")
} }
val disposable = Observable.fromCallable { val disposable = Observable.fromCallable {
@ -389,10 +418,22 @@ class DownloaderService : Service() {
.subscribeOn(Schedulers.newThread()) .subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({
val destDir = Uri.parse(getDownloadLocation(type)).run { val workTag = video.getURL()
DocumentsContract.buildChildDocumentsUriUsingTree(this, DocumentsContract.getTreeDocumentId(this)) val workData = workDataOf(
} originDir to tempFileDir.absolutePath,
fileUtil.moveFile(filesDir, context!!, destDir) downLocation to downloadLocation,
title to video.title
)
val fileTransferWorkRequest = OneTimeWorkRequestBuilder<FileTransferWorker>()
.addTag(workTag)
.setInputData(workData)
.build()
workManager.enqueueUniqueWork(
workTag,
ExistingWorkPolicy.KEEP,
fileTransferWorkRequest
)
downloadInfo.downloadPath = fileUtil.formatPath(getDownloadLocation(type)!!) downloadInfo.downloadPath = fileUtil.formatPath(getDownloadLocation(type)!!)
Log.e(TAG, downloadInfo.downloadPath) Log.e(TAG, downloadInfo.downloadPath)
downloadInfo.downloadType = type downloadInfo.downloadType = type
@ -416,10 +457,15 @@ class DownloaderService : Service() {
downloadInfo.downloadQueue = videos downloadInfo.downloadQueue = videos
startDownload(videos) startDownload(videos)
}) { e: Throwable? -> }) { e: Throwable? ->
if (BuildConfig.DEBUG) Log.e(TAG, getString(R.string.failed_download), e) tempFileDir.delete()
if (BuildConfig.DEBUG) {
Toast.makeText(context, e!!.message, Toast.LENGTH_LONG).show()
Log.e(TAG, getString(R.string.failed_download), e)
}
notificationUtil.updateDownloadNotification( notificationUtil.updateDownloadNotification(
NotificationUtil.DOWNLOAD_NOTIFICATION_ID, NotificationUtil.DOWNLOAD_NOTIFICATION_ID,
getString(R.string.failed_download), 0, 0, downloadQueue.peek()?.title getString(R.string.failed_download), 0, 0, downloadQueue.peek()?.title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
) )
downloadInfo.downloadType = type downloadInfo.downloadType = type
try { try {
@ -445,9 +491,22 @@ class DownloaderService : Service() {
private fun startCommandDownload(text: String?) { private fun startCommandDownload(text: String?) {
var text = text var text = text
if (text!!.startsWith("yt-dlp")) { if (text!!.startsWith("yt-dlp ")) {
text = text.substring(5).trim { it <= ' ' } text = text.substring(6).trim { it <= ' ' }
} }
val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE)
val downloadLocation = sharedPreferences.getString("command_path", getString(R.string.command_path))
var tempFileDir = File(cacheDir.absolutePath + "/command")
tempFileDir.delete()
tempFileDir.mkdir()
if (downloadLocation.equals(getString(R.string.command_path))){
tempFileDir = File(getString(R.string.command_path))
}
val request = YoutubeDLRequest(emptyList()) val request = YoutubeDLRequest(emptyList())
val commandRegex = "\"([^\"]*)\"|(\\S+)" val commandRegex = "\"([^\"]*)\"|(\\S+)"
val m = Pattern.compile(commandRegex).matcher(text) val m = Pattern.compile(commandRegex).matcher(text)
@ -458,8 +517,7 @@ class DownloaderService : Service() {
request.addOption(m.group(2)) request.addOption(m.group(2))
} }
} }
val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE) request.addOption("-o", tempFileDir.absolutePath + "/%(title)s.%(ext)s")
request.addOption("-o", filesDir.absolutePath + "/%(title)s.%(ext)s")
val disposable = Observable.fromCallable { val disposable = Observable.fromCallable {
YoutubeDL.getInstance().execute(request, downloadProcessID, callback) YoutubeDL.getInstance().execute(request, downloadProcessID, callback)
} }
@ -467,12 +525,22 @@ class DownloaderService : Service() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ youtubeDLResponse: YoutubeDLResponse -> .subscribe({ youtubeDLResponse: YoutubeDLResponse ->
downloadInfo.outputLine = youtubeDLResponse.out downloadInfo.outputLine = youtubeDLResponse.out
val downloadsDir = sharedPreferences.getString("command_path", getString(R.string.command_path))
val destDir = Uri.parse(downloadsDir).run { val workTag = SystemClock.uptimeMillis().toString()
DocumentsContract.buildChildDocumentsUriUsingTree(this, DocumentsContract.getTreeDocumentId(this)) val workData = workDataOf(
} originDir to tempFileDir.absolutePath,
fileUtil.moveFile(filesDir, context!!, destDir) downLocation to downloadLocation,
downloadInfo.outputLine += "\n\nMoved File to ${fileUtil.formatPath(downloadsDir!!)}" title to ""
)
val fileTransferWorkRequest = OneTimeWorkRequestBuilder<FileTransferWorker>()
.addTag(workTag)
.setInputData(workData)
.build()
workManager.enqueueUniqueWork(
workTag,
ExistingWorkPolicy.KEEP,
fileTransferWorkRequest
)
try { try {
for (activity in activities.keys) { for (activity in activities.keys) {
@ -489,6 +557,7 @@ class DownloaderService : Service() {
} }
}) { e: Throwable -> }) { e: Throwable ->
downloadInfo.outputLine = e.message downloadInfo.outputLine = e.message
tempFileDir.delete()
try { try {
for (activity in activities.keys) { for (activity in activities.keys) {
activity.runOnUiThread { activity.runOnUiThread {

View file

@ -30,6 +30,7 @@ import com.deniscerri.ytdlnis.service.IDownloaderService
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
import java.io.Reader import java.io.Reader
import java.nio.charset.Charset import java.nio.charset.Charset
@ -315,6 +316,17 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun createDefaultFolders(){
val audio = File(getString(R.string.music_path))
val video = File(getString(R.string.video_path))
val command = File(getString(R.string.command_path))
audio.mkdirs()
video.mkdirs()
command.mkdirs()
}
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
requestCode: Int, requestCode: Int,
permissions: Array<String>, permissions: Array<String>,
@ -325,6 +337,8 @@ class MainActivity : AppCompatActivity() {
for (i in permissions.indices) { for (i in permissions.indices) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) { if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
createPermissionRequestDialog() createPermissionRequestDialog()
}else{
createDefaultFolders()
} }
} }
} }

View file

@ -76,7 +76,14 @@ public class CustomCommandActivity extends AppCompatActivity {
} }
public void onDownloadProgress(DownloadInfo info) { public void onDownloadProgress(DownloadInfo info) {
output.append("\n" + info.getOutputLine() + "\n"); String newInfo = info.getOutputLine();
if (newInfo.contains("[download]")){
String temp = output.getText().toString();
output.setText(
temp.substring(0, temp.lastIndexOf(System.getProperty("line.separator")) - 2)
);
}
output.append(info.getOutputLine() + " \n");
output.scrollTo(0, output.getHeight()); output.scrollTo(0, output.getHeight());
scrollView.fullScroll(View.FOCUS_DOWN); scrollView.fullScroll(View.FOCUS_DOWN);
} }

View file

@ -162,6 +162,7 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
} }
databaseManager = new DatabaseManager(context); databaseManager = new DatabaseManager(context);
//TODO observe file transfer worker here and then update, but first you need to turn this to kotlin and implement proper mvvm
try { try {
item.setDownloadedTime(downloadedTime); item.setDownloadedTime(downloadedTime);
item.setDownloadPath(downloadPath); item.setDownloadPath(downloadPath);

View file

@ -85,10 +85,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
editor.putInt("concurrent_fragments", concurrentFragments!!.value) editor.putInt("concurrent_fragments", concurrentFragments!!.value)
editor.putString("limit_rate", limitRate!!.text) editor.putString("limit_rate", limitRate!!.text)
editor.putBoolean("aria2", aria2!!.isChecked) editor.putBoolean("aria2", aria2!!.isChecked)
editor.putString( editor.putStringSet("sponsorblock_filters", sponsorblockFilters!!.values)
"sponsorblock_filter",
java.lang.String.join(",", sponsorblockFilters!!.values)
)
editor.putBoolean("embed_subtitles", embedSubtitles!!.isChecked) editor.putBoolean("embed_subtitles", embedSubtitles!!.isChecked)
editor.putBoolean("embed_thumbnail", embedThumbnail!!.isChecked) editor.putBoolean("embed_thumbnail", embedThumbnail!!.isChecked)
editor.putBoolean("add_chapters", addChapters!!.isChecked) editor.putBoolean("add_chapters", addChapters!!.isChecked)
@ -178,7 +175,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
sponsorblockFilters!!.onPreferenceChangeListener = sponsorblockFilters!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
sponsorblockFilters!!.values = newValue as Set<String?>? sponsorblockFilters!!.values = newValue as Set<String?>?
editor.putString("sponsorblock_filter", java.lang.String.join(",", newValue)) editor.putStringSet("sponsorblock_filters", newValue)
editor.apply() editor.apply()
true true
} }

View file

@ -3,13 +3,17 @@ package com.deniscerri.ytdlnis.util
import android.content.Context import android.content.Context
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.FileUtils
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast
import com.deniscerri.ytdlnis.DownloaderService import com.deniscerri.ytdlnis.DownloaderService
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.Video import com.deniscerri.ytdlnis.database.Video
import java.io.File import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@ -77,9 +81,8 @@ class FileUtil() {
return context.getString(R.string.unfound_file); return context.getString(R.string.unfound_file);
} }
fun moveFile(originDir : File, context:Context, destDir : Uri, progress: (p: Int) -> Unit){
fun moveFile(originDir : File, context:Context, destDir : Uri){ originDir.listFiles()?.forEach {
originDir.listFiles().forEach {
val mimeType = val mimeType =
MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*" MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*"
@ -96,13 +99,28 @@ class FileUtil() {
) ?: return@forEach ) ?: return@forEach
val inputStream = it.inputStream() val inputStream = it.inputStream()
val outputStream = val outputStream = context.contentResolver.openOutputStream(destUri) ?: return@forEach
context.contentResolver.openOutputStream(destUri) ?: return@forEach val fileLength = it.length()
inputStream.copyTo(outputStream) var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = inputStream.read(buffer)
try {
while (bytes >= 0) {
outputStream.write(buffer, 0, bytes)
bytesCopied += bytes
progress((bytesCopied * 100 / fileLength).toInt())
bytes = inputStream.read(buffer)
}
}catch (e : Exception){
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
inputStream.close() inputStream.close()
outputStream.close() outputStream.close()
it.delete() it.delete()
} }
originDir.delete()
} }
} }

View file

@ -13,8 +13,9 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.receiver.NotificationReceiver import com.deniscerri.ytdlnis.receiver.NotificationReceiver
class NotificationUtil(var context: Context) { class NotificationUtil(var context: Context) {
private var notificationBuilder: NotificationCompat.Builder = private val downloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_SERVICE_CHANNEL_ID)
NotificationCompat.Builder(context, DOWNLOAD_SERVICE_CHANNEL_ID) private val commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID)
private val fileTransferNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, FILE_TRANSFER_CHANNEL_ID)
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
fun createNotificationChannel() { fun createNotificationChannel() {
@ -38,13 +39,63 @@ class NotificationUtil(var context: Context) {
channel = NotificationChannel(COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID, name, importance) channel = NotificationChannel(COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID, name, importance)
channel.description = description channel.description = description
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
//file transfers
name = context.getString(R.string.file_transfer_notification_channel_name)
description = context.getString(R.string.file_transfer_notification_channel_description)
channel = NotificationChannel(FILE_TRANSFER_CHANNEL_ID, name, importance)
channel.description = description
notificationManager.createNotificationChannel(channel)
} }
} }
fun createFileTransferNotification(
pendingIntent: PendingIntent?,
title: String?,
) : Notification {
val intent = Intent(context, NotificationReceiver::class.java)
return fileTransferNotificationBuilder
.setContentTitle(title)
.setCategory(Notification.CATEGORY_PROGRESS)
.setSmallIcon(R.drawable.ic_app_icon)
.setContentText("")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setProgress(PROGRESS_MAX, PROGRESS_CURR, false)
.setContentIntent(pendingIntent)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
}
fun updateFileTransferNotification(
id: Int,
progress: Int
) {
try {
fileTransferNotificationBuilder.setProgress(100, progress, false)
notificationManager.notify(id, fileTransferNotificationBuilder.build())
} catch (ignored: Exception) {
}
}
private fun getBuilder(channel: String) : NotificationCompat.Builder {
when(channel) {
DOWNLOAD_SERVICE_CHANNEL_ID -> { return downloadNotificationBuilder}
COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID -> { return commandDownloadNotificationBuilder }
FILE_TRANSFER_CHANNEL_ID -> { return fileTransferNotificationBuilder }
}
return downloadNotificationBuilder
}
fun createDownloadServiceNotification( fun createDownloadServiceNotification(
pendingIntent: PendingIntent?, pendingIntent: PendingIntent?,
title: String? title: String?,
channel: String
): Notification { ): Notification {
val notificationBuilder = getBuilder(channel)
val intent = Intent(context, NotificationReceiver::class.java) val intent = Intent(context, NotificationReceiver::class.java)
intent.putExtra("cancel", "") intent.putExtra("cancel", "")
val cancelNotificationPendingIntent = PendingIntent.getBroadcast( val cancelNotificationPendingIntent = PendingIntent.getBroadcast(
@ -80,17 +131,21 @@ class NotificationUtil(var context: Context) {
desc: String, desc: String,
progress: Int, progress: Int,
queue: Int, queue: Int,
title: String? title: String?,
channel : String
) { ) {
val notificationBuilder = getBuilder(channel)
var contentText = "" var contentText = ""
if (queue > 1) contentText += """${queue - 1} ${context.getString(R.string.items_left)}""" if (queue > 1) contentText += """${queue - 1} ${context.getString(R.string.items_left)}""" + "\n"
contentText += desc.replace("\\[.*?\\] ".toRegex(), "") contentText += desc.replace("\\[.*?\\] ".toRegex(), "")
try { try {
notificationBuilder.setProgress(100, progress, false) notificationBuilder.setProgress(100, progress, false)
.setContentTitle(title) .setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
notificationManager.notify(id, notificationBuilder.build()) notificationManager.notify(id, notificationBuilder.build())
} catch (ignored: Exception) { } catch (e: Exception) {
e.printStackTrace()
} }
} }
@ -103,6 +158,7 @@ class NotificationUtil(var context: Context) {
const val DOWNLOAD_NOTIFICATION_ID = 1 const val DOWNLOAD_NOTIFICATION_ID = 1
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2" const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"
const val COMMAND_DOWNLOAD_NOTIFICATION_ID = 2 const val COMMAND_DOWNLOAD_NOTIFICATION_ID = 2
const val FILE_TRANSFER_CHANNEL_ID = "3"
private const val PROGRESS_MAX = 100 private const val PROGRESS_MAX = 100
private const val PROGRESS_CURR = 0 private const val PROGRESS_CURR = 0
} }

View file

@ -24,6 +24,7 @@ import java.io.InputStreamReader
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import java.io.File
class UpdateUtil(var context: Context) { class UpdateUtil(var context: Context) {
private val tag = "UpdateUtil" private val tag = "UpdateUtil"
@ -163,11 +164,12 @@ class UpdateUtil(var context: Context) {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ status: UpdateStatus -> .subscribe({ status: UpdateStatus ->
when (status) { when (status) {
UpdateStatus.DONE -> Toast.makeText( UpdateStatus.DONE ->
context, Toast.makeText(
context.getString(R.string.ytld_update_success), context,
Toast.LENGTH_LONG context.getString(R.string.ytld_update_success),
).show() Toast.LENGTH_LONG
).show()
UpdateStatus.ALREADY_UP_TO_DATE -> Toast.makeText( UpdateStatus.ALREADY_UP_TO_DATE -> Toast.makeText(
context, context,
context.getString(R.string.you_are_in_latest_version), context.getString(R.string.you_are_in_latest_version),

View file

@ -0,0 +1,55 @@
package com.deniscerri.ytdlnis.work
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.SystemClock
import android.provider.DocumentsContract
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.page.CustomCommandActivity
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import java.io.File
class FileTransferWorker(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val originDir = File(inputData.getString(originDir)!!)
val downLocation = inputData.getString(downLocation)
val fileTitle = inputData.getString(title) ?: ""
val destDir = Uri.parse(downLocation).run {
DocumentsContract.buildChildDocumentsUriUsingTree(this, DocumentsContract.getTreeDocumentId(this))
}
val fileUtil = FileUtil()
val notificationUtil = NotificationUtil(context)
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val title = "Moving $fileTitle to ${fileUtil.formatPath(downLocation!!)}"
val id : Int = SystemClock.uptimeMillis().toInt()
val notification = notificationUtil.createFileTransferNotification(pendingIntent, title)
val foregroundInfo = ForegroundInfo(id, notification)
setForeground(foregroundInfo)
fileUtil.moveFile(originDir, context, destDir){ progress ->
notificationUtil.updateFileTransferNotification(id, progress)
}
return Result.success()
}
companion object {
const val downLocation = "downLocation"
const val originDir = "originDir"
const val title = "title"
}
}

View file

@ -49,7 +49,7 @@
<TextView <TextView
android:id="@+id/result_title" android:id="@+id/result_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="120dp"
android:paddingTop="10dp" android:paddingTop="10dp"
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:paddingBottom="20dp" android:paddingBottom="20dp"

View file

@ -65,9 +65,9 @@
<string name="choose_range_desc">Pick a range to download from the playlist (to not download everything).</string> <string name="choose_range_desc">Pick a range to download from the playlist (to not download everything).</string>
<string name="last_cant_be_smaller_than_first">Last index cannot be smaller than the first.</string> <string name="last_cant_be_smaller_than_first">Last index cannot be smaller than the first.</string>
<string name="first_cant_be_larger_than_last">First index cannot be larger than the last.</string> <string name="first_cant_be_larger_than_last">First index cannot be larger than the last.</string>
<string name="music_path" translatable="false">/storage/emulated/0/Download</string> <string name="music_path" translatable="false">/storage/emulated/0/Download/YTDLnis/Audio</string>
<string name="video_path" translatable="false">/storage/emulated/0/Download</string> <string name="video_path" translatable="false">/storage/emulated/0/Download/YTDLnis/Video</string>
<string name="command_path" translatable="false">/storage/emulated/0/Download</string> <string name="command_path" translatable="false">/storage/emulated/0/Download/YTDLnis/Command</string>
<string name="concurrent_fragments_summary">Number of fragments of a DASH/HLS native video to download concurrently</string> <string name="concurrent_fragments_summary">Number of fragments of a DASH/HLS native video to download concurrently</string>
<string name="concurrent_fragments">Concurrent Fragments</string> <string name="concurrent_fragments">Concurrent Fragments</string>
<string name="downloading">Downloading</string> <string name="downloading">Downloading</string>
@ -149,4 +149,6 @@
<string name="sponsorblock_reminders">Subscription Reminders</string> <string name="sponsorblock_reminders">Subscription Reminders</string>
<string name="select_sponsorblock_filtering">Select SponsorBlock Filtering</string> <string name="select_sponsorblock_filtering">Select SponsorBlock Filtering</string>
<string name="unfound_file" translatable="false">/404.404</string> <string name="unfound_file" translatable="false">/404.404</string>
<string name="file_transfer_notification_channel_name">File Transfers</string>
<string name="file_transfer_notification_channel_description">Notification showing the progress of moving files from the temporary folder to set destination</string>
</resources> </resources>

View file

@ -5,7 +5,6 @@
<item name="android:windowLightStatusBar">?attr/isLightTheme</item> <item name="android:windowLightStatusBar">?attr/isLightTheme</item>
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:color">@android:color/transparent</item> <item name="android:color">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
</style> </style>
<style name="AppTheme" parent="AppDefaultTheme" /> <style name="AppTheme" parent="AppDefaultTheme" />

View file

@ -25,6 +25,7 @@ buildscript {
workVer = "2.7.1" workVer = "2.7.1"
composeVer = "1.3.2" composeVer = "1.3.2"
kotlinVer = "1.7.20" kotlinVer = "1.7.20"
coroutineVer = "1.6.4"
} }
repositories { repositories {
mavenCentral() mavenCentral()