diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml index a53c62e8..4f9550fb 100644 --- a/.idea/assetWizardSettings.xml +++ b/.idea/assetWizardSettings.xml @@ -320,7 +320,7 @@ @@ -331,7 +331,7 @@ diff --git a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/1.json b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/1.json index 5fe6d706..cc350343 100644 --- a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/1.json +++ b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "6c48acc2155f9a895971bd84c84b7d4e", + "identityHash": "5939be553c41327f4f64e1d62d008968", "entities": [ { "tableName": "results", @@ -198,7 +198,7 @@ }, { "tableName": "downloads", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `audioQualityId` TEXT NOT NULL, `videoQualityId` TEXT NOT NULL, `customTemplateId` INTEGER NOT NULL, `audioFormat` TEXT NOT NULL, `videoFormat` TEXT NOT NULL, `formatDesc` TEXT NOT NULL, `removeAudio` INTEGER NOT NULL DEFAULT 0, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `workID` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `audioQualityId` TEXT NOT NULL, `videoQualityId` TEXT NOT NULL, `customTemplateId` INTEGER NOT NULL, `audioFormat` TEXT NOT NULL, `videoFormat` TEXT NOT NULL, `formatDesc` TEXT NOT NULL, `removeAudio` INTEGER NOT NULL DEFAULT 0, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `embedSubs` INTEGER NOT NULL, `addChapters` INTEGER NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `workID` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", "fields": [ { "fieldPath": "url", @@ -303,6 +303,24 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "embedSubs", + "columnName": "embedSubs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addChapters", + "columnName": "addChapters", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "SaveThumb", + "columnName": "SaveThumb", + "affinity": "INTEGER", + "notNull": true + }, { "fieldPath": "status", "columnName": "status", @@ -368,7 +386,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6c48acc2155f9a895971bd84c84b7d4e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5939be553c41327f4f64e1d62d008968')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/App.kt b/app/src/main/java/com/deniscerri/ytdlnis/App.kt index c8cddd94..708c5679 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/App.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/App.kt @@ -9,6 +9,8 @@ import android.os.Build import android.util.Log import android.widget.Toast import androidx.preference.PreferenceManager +import androidx.work.Configuration +import androidx.work.WorkManager import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.UpdateUtil import com.google.android.material.color.DynamicColors @@ -23,12 +25,12 @@ import io.reactivex.observers.DisposableCompletableObserver import io.reactivex.plugins.RxJavaPlugins import io.reactivex.schedulers.Schedulers import java.io.File +import java.util.concurrent.Executors class App : Application() { override fun onCreate() { super.onCreate() DynamicColors.applyToActivitiesIfAvailable(this) - notificationUtil = NotificationUtil(this) createNotificationChannels() PreferenceManager.setDefaultValues( this, @@ -37,6 +39,15 @@ class App : Application() { R.xml.root_preferences, false ) + + WorkManager.initialize( + this@App, + Configuration.Builder() + .setExecutor(Executors.newFixedThreadPool( + getSharedPreferences("root_preferences", MODE_PRIVATE) + .getInt("concurrent_downloads", 1))) + .build()) + configureRxJavaErrorHandler() Completable.fromAction { initLibraries() }.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -79,12 +90,11 @@ class App : Application() { } private fun createNotificationChannels() { + val notificationUtil = NotificationUtil(this) notificationUtil.createNotificationChannel() } companion object { private const val TAG = "App" - @SuppressLint("StaticFieldLeak") - lateinit var notificationUtil: NotificationUtil } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.kt b/app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.kt index e031014e..a956a58c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.kt @@ -9,10 +9,7 @@ import android.os.Build import android.os.IBinder import android.os.SystemClock import android.util.Log -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.workDataOf +import androidx.work.* import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.service.DownloadInfo import com.deniscerri.ytdlnis.service.IDownloaderListener @@ -32,6 +29,7 @@ import io.reactivex.schedulers.Schedulers import java.io.File import java.util.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors import java.util.regex.Pattern @@ -43,7 +41,6 @@ class DownloaderService : Service() { private val downloadInfo = DownloadInfo() private var downloadQueue = LinkedList() private val compositeDisposable = CompositeDisposable() - private val notificationUtil = App.notificationUtil private var context: Context? = null var downloadProcessID = "processID" private var downloadNotificationID = 0 @@ -59,11 +56,11 @@ class DownloaderService : Service() { if (!downloadQueue.isEmpty()) { title = downloadQueue.peek()?.title } - notificationUtil.updateDownloadNotification( - downloadNotificationID, - line!!, progress.toInt(), downloadQueue.size, title, - notificationChannelID - ) +// notificationUtil.updateDownloadNotification( +// downloadNotificationID, +// line!!, progress.toInt(), downloadQueue.size, title, +// notificationChannelID +// ) try { for (activity in activities.keys) { activity.runOnUiThread { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ConfigureMultipleDownloadsAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ConfigureMultipleDownloadsAdapter.kt new file mode 100644 index 00000000..f93e54f6 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ConfigureMultipleDownloadsAdapter.kt @@ -0,0 +1,125 @@ +package com.deniscerri.ytdlnis.adapter + +import android.app.Activity +import android.graphics.Color +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.database.models.DownloadItem +import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView +import com.squareup.picasso.Picasso + +class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : + ListAdapter(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { + private val onItemClickListener: OnItemClickListener + private val activity: Activity + + init { + this.onItemClickListener = onItemClickListener + this.activity = activity + } + + class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : + RecyclerView.ViewHolder(itemView) { + val cardView: MaterialCardView + + init { + cardView = itemView.findViewById(R.id.download_multiple_card_view) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.home_download_multiple_card, parent, false) + return ViewHolder(cardView, onItemClickListener) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + val card = holder.cardView + // THUMBNAIL ---------------------------------- + val thumbnail = card.findViewById(R.id.result_image_view) + val imageURL = item!!.thumb + if (imageURL.isNotEmpty()) { + val uiHandler = Handler(Looper.getMainLooper()) + uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) } + thumbnail.setColorFilter(Color.argb(70, 0, 0, 0)) + } else { + val uiHandler = Handler(Looper.getMainLooper()) + uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) } + thumbnail.setColorFilter(Color.argb(70, 0, 0, 0)) + } + + // TITLE ---------------------------------- + val itemTitle = card.findViewById(R.id.result_title) + var title = item.title + if (title.length > 100) { + title = title.substring(0, 40) + "..." + } + itemTitle.text = title + + // Author ---------------------------------- + val author = card.findViewById(R.id.author) + author.text = item.author + + // Format + val format = card.findViewById(R.id.format) + format.text = item.ext + + // Quality + val quality = card.findViewById(R.id.quality) + quality.text = item.formatDesc + + // File Size + val fileSize = card.findViewById(R.id.filesize) + fileSize.text = item.filesize + + // Type Icon Button + val btn = card.findViewById(R.id.download_type) + + when(item.type) { + "audio" -> { + btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_music) + } + "video" -> { + btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_video) + } + "command" -> { + btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_baseline_keyboard_arrow_right_24) + } + } + + card.setOnClickListener { + onItemClickListener.onCardClick(item.id!!) + } + } + + + interface OnItemClickListener { + fun onCardClick(itemID: Int) + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { + return oldItem.id === newItem.id + } + + override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { + return oldItem.url == newItem.url + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index ea049c8b..34dd371d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -15,8 +15,11 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloads() : LiveData> + @Query("SELECT * FROM downloads WHERE status='Queued'") + fun getQueuedDownloads() : List + @Query("SELECT * FROM downloads WHERE workID=:workID") - fun getDownloadsByWorkId(workID: Int) : List + fun getDownloadByWorkId(workID: Int) : DownloadItem @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") fun getDownloadById(id: Int) : DownloadItem diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt index 073609b2..75848851 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt @@ -27,8 +27,10 @@ data class DownloadItem( val embedSubs: Boolean, val addChapters: Boolean, val SaveThumb: Boolean, + var ext: String, + var filesize: String, @ColumnInfo(defaultValue = "Queued") - val status: String, + var status: String, val workID: Int ){ @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt index 0005803d..b06bbfc3 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt @@ -8,6 +8,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { val allDownloads : LiveData> = downloadDao.getAllDownloads() val activeDownloads : LiveData> = downloadDao.getActiveDownloads() + enum class status { + Active, Queued, Errored + } + suspend fun insert(item: DownloadItem){ downloadDao.insert(item) } @@ -20,9 +24,17 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.update(item) } - fun getDownloadsByWorkID(workID: Int) : List{ - return downloadDao.getDownloadsByWorkId(workID) + fun getDownloadByWorkID(workID: Int) : DownloadItem{ + return downloadDao.getDownloadByWorkId(workID) } + fun getQueuedDownloads() : List{ + return downloadDao.getQueuedDownloads() + } + + suspend fun setDownloadStatus(item: DownloadItem, status: status){ + item.status = status.toString() + update(item); + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/settings/SettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/settings/SettingsFragment.kt index ee8936d3..4ff4aece 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/settings/SettingsFragment.kt @@ -20,6 +20,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private var downloadCard: SwitchPreferenceCompat? = null private var apiKey: EditTextPreference? = null private var concurrentFragments: SeekBarPreference? = null + private var concurrentDownloads: SeekBarPreference? = null private var limitRate: EditTextPreference? = null private var aria2: SwitchPreferenceCompat? = null private var sponsorblockFilters: MultiSelectListPreference? = null @@ -55,6 +56,7 @@ class SettingsFragment : PreferenceFragmentCompat() { downloadCard = findPreference("download_card") apiKey = findPreference("api_key") concurrentFragments = findPreference("concurrent_fragments") + concurrentDownloads = findPreference("concurrent_downloads") limitRate = findPreference("limit_rate") aria2 = findPreference("aria2") sponsorblockFilters = findPreference("sponsorblock_filter") @@ -83,6 +85,7 @@ class SettingsFragment : PreferenceFragmentCompat() { editor.putBoolean("download_card", downloadCard!!.isChecked) editor.putString("api_key", apiKey!!.text) editor.putInt("concurrent_fragments", concurrentFragments!!.value) + editor.putInt("concurrent_downloads", concurrentDownloads!!.value) editor.putString("limit_rate", limitRate!!.text) editor.putBoolean("aria2", aria2!!.isChecked) editor.putStringSet("sponsorblock_filters", sponsorblockFilters!!.values) @@ -159,6 +162,13 @@ class SettingsFragment : PreferenceFragmentCompat() { editor.apply() true } + concurrentDownloads!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + val value = newValue.toString().toInt() + editor.putInt("concurrent_downloads", value) + editor.apply() + true + } limitRate!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> editor.putString("limit_rate", newValue.toString()) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt index b7f7015e..345b1836 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -9,21 +9,21 @@ import android.os.SystemClock import android.provider.DocumentsContract import android.util.Log import android.widget.Toast -import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo +import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.workDataOf import com.deniscerri.ytdlnis.BuildConfig import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager -import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.NotificationUtil import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDLRequest +import kotlinx.coroutines.runBlocking import java.io.File import java.util.regex.Pattern @@ -31,8 +31,8 @@ import java.util.regex.Pattern class DownloadWorker( private val context: Context, workerParams: WorkerParameters -) : CoroutineWorker(context, workerParams) { - override suspend fun doWork(): Result { +) : Worker(context, workerParams) { + override fun doWork(): Result { workID = inputData.getInt("workID", SystemClock.uptimeMillis().toInt()) val notificationUtil = NotificationUtil(context) val dbManager = DBManager.getInstance(context) @@ -41,183 +41,182 @@ class DownloadWorker( val commandTemplateDao = dbManager.commandTemplateDao val historyDao = dbManager.historyDao - queue = repository.getDownloadsByWorkID(workID) as MutableList - + val downloadItem = repository.getDownloadByWorkID(workID) + runBlocking{ + repository.setDownloadStatus(downloadItem, DownloadRepository.status.Active) + } val intent = Intent(context, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, queue[0].title, workID, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) + val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, workID, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) val foregroundInfo = ForegroundInfo(workID, notification) - setForeground(foregroundInfo) + setForegroundAsync(foregroundInfo) - while (queue.isNotEmpty()){ - val item = dao.getDownloadById(queue[0].id!!) - queue.removeFirst() - val url = item.url - val request = YoutubeDLRequest(url) - val type = item.type - val downloadLocation = item.downloadPath + + val url = downloadItem.url + val request = YoutubeDLRequest(url) + val type = downloadItem.type + val downloadLocation = downloadItem.downloadPath - val tempFolder = StringBuilder(context.cacheDir.absolutePath + """/${item.title}##${item.type}""") - when(type){ - "audio" -> tempFolder.append("##${item.audioQualityId}") - "video" -> tempFolder.append("##${item.videoQualityId}") - "command" -> tempFolder.append("##${item.customTemplateId}") + val tempFolder = StringBuilder(context.cacheDir.absolutePath + """/${downloadItem.title}##${downloadItem.type}""") + when(type){ + "audio" -> tempFolder.append("##${downloadItem.audioQualityId}") + "video" -> tempFolder.append("##${downloadItem.videoQualityId}") + "command" -> tempFolder.append("##${downloadItem.customTemplateId}") + } + var tempFileDir = File(tempFolder.toString()) + tempFileDir.delete() + tempFileDir.mkdir() + + val sharedPreferences = context.getSharedPreferences("root_preferences", + Service.MODE_PRIVATE + ) + val aria2 = sharedPreferences.getBoolean("aria2", false) + if (aria2) { + request.addOption("--downloader", "libaria2c.so") + request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"") + } else { + val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1) + if (concurrentFragments > 1) request.addOption("-N", concurrentFragments) + } + val limitRate = sharedPreferences.getString("limit_rate", "") + if (limitRate != "") request.addOption("-r", limitRate!!) + val writeThumbnail = sharedPreferences.getBoolean("write_thumbnail", false) + if (writeThumbnail) { + request.addOption("--write-thumbnail") + request.addOption("--convert-thumbnails", "png") + } + request.addOption("--no-mtime") + val sponsorBlockFilters = sharedPreferences.getStringSet("sponsorblock_filters", emptySet()) + if (sponsorBlockFilters!!.isNotEmpty()) { + val filters = java.lang.String.join(",", sponsorBlockFilters) + request.addOption("--sponsorblock-remove", filters) + } + + when(type){ + "audio" -> { + request.addOption("-x") + var audioQualityId : String = downloadItem.audioQualityId + if (audioQualityId == "0") audioQualityId = "ba" + var format = downloadItem.audioFormat + if (format.isEmpty()){ + format = sharedPreferences.getString("audio_format", "")!! + } + request.addOption("-f", audioQualityId) + request.addOption("--audio-format", format) + request.addOption("--embed-metadata") + + val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) + if (embedThumb) { + request.addOption("--embed-thumbnail") + request.addOption("--convert-thumbnails", "png") + try { + val config = File(context.cacheDir, "config" + downloadItem.title + "##" + downloadItem.audioQualityId + ".txt") + val configData = + "--ppa \"ffmpeg: -c:v png -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" + config.writeText(configData) + request.addOption("--config", config.absolutePath) + } catch (ignored: Exception) {} + } + request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s") + request.addCommands(listOf("--replace-in-metadata", "title", ".*.", downloadItem.title)) + request.addCommands(listOf("--replace-in-metadata", "uploader", ".*.", downloadItem.author)) + + if (downloadItem.playlistTitle.isNotEmpty()) { + request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s") + request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track)s") + } else { + request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") + } + request.addOption("-o", tempFileDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s") } - var tempFileDir = File(tempFolder.toString()) - tempFileDir.delete() - tempFileDir.mkdir() + "video" -> { + if (downloadLocation == context.getString(R.string.video_path)){ + tempFileDir = File(context.getString(R.string.video_path)) + } - val sharedPreferences = context.getSharedPreferences("root_preferences", - Service.MODE_PRIVATE - ) - val aria2 = sharedPreferences.getBoolean("aria2", false) - if (aria2) { - request.addOption("--downloader", "libaria2c.so") - request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"") - } else { - val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1) - if (concurrentFragments > 1) request.addOption("-N", concurrentFragments) - } - val limitRate = sharedPreferences.getString("limit_rate", "") - if (limitRate != "") request.addOption("-r", limitRate!!) - val writeThumbnail = sharedPreferences.getBoolean("write_thumbnail", false) - if (writeThumbnail) { - request.addOption("--write-thumbnail") - request.addOption("--convert-thumbnails", "png") - } - request.addOption("--no-mtime") - val sponsorBlockFilters = sharedPreferences.getStringSet("sponsorblock_filters", emptySet()) - if (sponsorBlockFilters!!.isNotEmpty()) { - val filters = java.lang.String.join(",", sponsorBlockFilters) - request.addOption("--sponsorblock-remove", filters) - } - - when(type){ - "audio" -> { - request.addOption("-x") - var audioQualityId : String = item.audioQualityId - if (audioQualityId == "0") audioQualityId = "ba" - var format = item.audioFormat - if (format.isEmpty()){ - format = sharedPreferences.getString("audio_format", "")!! - } - request.addOption("-f", audioQualityId) - request.addOption("--audio-format", format) - request.addOption("--embed-metadata") + val addChapters = sharedPreferences.getBoolean("add_chapters", false) + if (addChapters) { + request.addOption("--sponsorblock-mark", "all") + } + val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false) + if (embedSubs) { + request.addOption("--embed-subs", "") + } + var videoQualityId = downloadItem.videoQualityId + val audioQualityId = downloadItem.audioQualityId + if (videoQualityId.isEmpty()) videoQualityId = "bestvideo" + val formatArgument = StringBuilder(videoQualityId) + if (videoQualityId != "worst"){ + if (audioQualityId != "0") formatArgument.append("+", audioQualityId, "/best") + } + request.addOption("-f", formatArgument.toString()) + var format = downloadItem.videoFormat + if (format.isNotEmpty()) { + format = sharedPreferences.getString("video_format", "")!! + if (format != "DEFAULT") request.addOption("--merge-output-format", format) + } + if (format != "webm") { val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) if (embedThumb) { request.addOption("--embed-thumbnail") - request.addOption("--convert-thumbnails", "png") - try { - val config = File(context.cacheDir, "config" + item.title + "##" + item.audioQualityId + ".txt") - val configData = - "--ppa \"ffmpeg: -c:v png -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" - config.writeText(configData) - request.addOption("--config", config.absolutePath) - } catch (ignored: Exception) {} - } - request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s") - request.addCommands(listOf("--replace-in-metadata", "title", ".*.", item.title)) - request.addCommands(listOf("--replace-in-metadata", "uploader", ".*.", item.author)) - - if (item.playlistTitle.isNotEmpty()) { - request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s") - request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track)s") - } else { - request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") - } - request.addOption("-o", tempFileDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s") - } - "video" -> { - if (downloadLocation == context.getString(R.string.video_path)){ - tempFileDir = File(context.getString(R.string.video_path)) - } - - val addChapters = sharedPreferences.getBoolean("add_chapters", false) - if (addChapters) { - request.addOption("--sponsorblock-mark", "all") - } - val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false) - if (embedSubs) { - request.addOption("--embed-subs", "") - } - var videoQualityId = item.videoQualityId - val audioQualityId = item.audioQualityId - if (videoQualityId.isEmpty()) videoQualityId = "bestvideo" - val formatArgument = StringBuilder(videoQualityId) - if (videoQualityId != "worst"){ - if (audioQualityId != "0") formatArgument.append("+", audioQualityId, "/best") - } - request.addOption("-f", formatArgument.toString()) - var format = item.videoFormat - if (format.isNotEmpty()) { - format = sharedPreferences.getString("video_format", "")!! - if (format != "DEFAULT") request.addOption("--merge-output-format", format) - } - - if (format != "webm") { - val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) - if (embedThumb) { - request.addOption("--embed-thumbnail") - } - } - request.addOption("-o", tempFileDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s") - } - "command" -> { - val commandRegex = "\"([^\"]*)\"|(\\S+)" - val command = commandTemplateDao.getTemplateById(item.customTemplateId) - val m = Pattern.compile(commandRegex).matcher(command.content) - while (m.find()) { - if (m.group(1) != null) { - request.addOption(m.group(1)!!) - } else { - request.addOption(m.group(2)!!) - } } } + request.addOption("-o", tempFileDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s") } - - runCatching { - YoutubeDL.getInstance().execute(request, item.id.toString()){ progress, _, line -> - setProgressAsync(workDataOf("progress" to progress.toInt())) - var title: String? = context.getString(R.string.download_start) - if (queue.isNotEmpty()) { - title = queue[0].title + "command" -> { + val commandRegex = "\"([^\"]*)\"|(\\S+)" + val command = commandTemplateDao.getTemplateById(downloadItem.customTemplateId) + val m = Pattern.compile(commandRegex).matcher(command.content) + while (m.find()) { + if (m.group(1) != null) { + request.addOption(m.group(1)!!) + } else { + request.addOption(m.group(2)!!) } - notificationUtil.updateDownloadNotification( - workID, - line, progress.toInt(), queue.size, title, - NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID - ) } - }.onSuccess { - //move file from internal to set download directory - moveFile(tempFileDir.absoluteFile, downloadLocation){ progress -> - setProgressAsync(workDataOf("progress" to progress)) - } - //put download in history - val incognito = sharedPreferences.getBoolean("incognito", false) - if (!incognito) { - val unixtime = System.currentTimeMillis() / 1000 - val historyItem = HistoryItem(item.url, item.title, item.author, item.duration, item.thumb, item.type, unixtime, item.downloadPath, item.website) - historyDao.insert(historyItem) - } - - }.onFailure { - tempFileDir.delete() - if (BuildConfig.DEBUG) { - Toast.makeText(context, it.message, Toast.LENGTH_LONG).show() - Log.e(DownloadWorker.TAG, context.getString(R.string.failed_download), it) - } - notificationUtil.updateDownloadNotification( - workID, - context.getString(R.string.failed_download), 0, 0, item.title, - NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID - ) - return Result.failure() } } + + runCatching { + YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> + setProgressAsync(workDataOf("progress" to progress.toInt())) + val title: String = downloadItem.title + val queue = dao.getQueuedDownloads() + notificationUtil.updateDownloadNotification( + workID, + line, progress.toInt(), queue.size, title, + NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID + ) + } + }.onSuccess { + //move file from internal to set download directory + moveFile(tempFileDir.absoluteFile, downloadLocation){ progress -> + setProgressAsync(workDataOf("progress" to progress)) + } + //put download in history + val incognito = sharedPreferences.getBoolean("incognito", false) + if (!incognito) { + val unixtime = System.currentTimeMillis() / 1000 + val historyItem = HistoryItem(downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, downloadItem.downloadPath, downloadItem.website) + runBlocking { + historyDao.insert(historyItem) + } + } + }.onFailure { + tempFileDir.delete() + if (BuildConfig.DEBUG) { + Toast.makeText(context, it.message, Toast.LENGTH_LONG).show() + Log.e(TAG, context.getString(R.string.failed_download), it) + } + notificationUtil.updateDownloadNotification( + workID, + context.getString(R.string.failed_download), 0, 0, downloadItem.title, + NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID + ) + return Result.failure() + } + return Result.success() } @@ -247,7 +246,6 @@ class DownloadWorker( companion object { var workID: Int = 0 const val TAG = "DownloadWorker" - lateinit var queue: MutableList } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_concurrent_downloads.xml b/app/src/main/res/drawable/ic_concurrent_downloads.xml new file mode 100644 index 00000000..68410f71 --- /dev/null +++ b/app/src/main/res/drawable/ic_concurrent_downloads.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 668ade1d..b3c40927 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -165,7 +165,4 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/home_download_audio_bottom_sheet.xml b/app/src/main/res/layout/home_download_audio_bottom_sheet.xml deleted file mode 100644 index 0547e925..00000000 --- a/app/src/main/res/layout/home_download_audio_bottom_sheet.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/home_download_all_bottom_sheet.xml b/app/src/main/res/layout/home_download_multiple_bottom_sheet.xml similarity index 76% rename from app/src/main/res/layout/home_download_all_bottom_sheet.xml rename to app/src/main/res/layout/home_download_multiple_bottom_sheet.xml index 8c986512..6b040e7d 100644 --- a/app/src/main/res/layout/home_download_all_bottom_sheet.xml +++ b/app/src/main/res/layout/home_download_multiple_bottom_sheet.xml @@ -7,6 +7,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + - - - -