made downloads work on OTG drives

This commit is contained in:
Denis Çerri 2023-01-06 22:49:44 +01:00
parent b83c73048b
commit bba9dbd96a
No known key found for this signature in database
GPG key ID: 96B3554AF5B193EE
16 changed files with 990 additions and 920 deletions

View file

@ -30,7 +30,7 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
archivesBaseName = "YTDLnis-$versionName"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
vectorDrawables {
useSupportLibrary true
@ -75,7 +75,7 @@ android {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a'
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
}

View file

@ -4,8 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="androidd.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<application
android:name=".App"
@ -16,7 +19,7 @@
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppDefaultTheme" >
android:theme="@style/AppTheme" >
<activity android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true">

View file

@ -1,519 +0,0 @@
package com.deniscerri.ytdlnis;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.deniscerri.ytdlnis.database.Video;
import com.deniscerri.ytdlnis.page.CustomCommandActivity;
import com.deniscerri.ytdlnis.service.DownloadInfo;
import com.deniscerri.ytdlnis.service.IDownloaderListener;
import com.deniscerri.ytdlnis.service.IDownloaderService;
import com.deniscerri.ytdlnis.util.NotificationUtil;
import com.yausername.youtubedl_android.DownloadProgressCallback;
import com.yausername.youtubedl_android.YoutubeDL;
import com.yausername.youtubedl_android.YoutubeDLRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class DownloaderService extends Service {
private LocalBinder binder = new LocalBinder();
private Map<Activity, ArrayList<IDownloaderListener>> activities = new ConcurrentHashMap<>();
private DownloadInfo downloadInfo = new DownloadInfo();
private LinkedList<Video> downloadQueue = new LinkedList<>();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private final NotificationUtil notificationUtil = App.notificationUtil;
private Context context;
public String downloadProcessID = "processID";
private static final String TAG = "DownloaderService";
private int downloadNotificationID;
private final DownloadProgressCallback callback = (progress, etaInSeconds, line) -> {
downloadInfo.setProgress((int) progress);
downloadInfo.setOutputLine(line);
downloadInfo.setDownloadQueue(downloadQueue);
String title = getString(R.string.running_ytdlp_command);
if (!downloadQueue.isEmpty()){
title = downloadQueue.peek().getTitle();
}
notificationUtil.updateDownloadNotification(downloadNotificationID,
line, (int) progress, downloadQueue.size(), title);
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
if (activities.get(activity) != null){
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadProgress(downloadInfo);
}
}
});
}
}catch (Exception ignored){}
};
@Override
public void onCreate() {
super.onCreate();
context = this;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Intent theIntent;
PendingIntent pendingIntent;
if(intent.getBooleanExtra("rebind", false)){
return binder;
}
int id = intent.getIntExtra("id", 1);
switch (id){
case NotificationUtil.DOWNLOAD_NOTIFICATION_ID:
theIntent = new Intent(this, MainActivity.class);
pendingIntent = PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE);
downloadNotificationID = id;
ArrayList<? extends Video> queue = intent.getParcelableArrayListExtra("queue");
downloadQueue = new LinkedList<>();
downloadQueue.addAll(queue);
downloadInfo.setDownloadQueue(downloadQueue);
String title = downloadInfo.getVideo().getTitle();
Notification notification = App.notificationUtil.createDownloadServiceNotification(pendingIntent,title);
startForeground(downloadNotificationID, notification);
startDownload(downloadQueue);
break;
case NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID:
theIntent = new Intent(this, CustomCommandActivity.class);
pendingIntent = PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE);
downloadNotificationID = id;
String command = intent.getStringExtra("command");
Notification command_notification = App.notificationUtil.createDownloadServiceNotification(pendingIntent,getString(R.string.running_ytdlp_command));
startForeground(downloadNotificationID, command_notification);
startCommandDownload(command);
break;
}
return binder;
}
@Override
public void onDestroy() {
super.onDestroy();
stopForeground(true);
stopSelf();
}
public class LocalBinder extends Binder implements IDownloaderService {
public DownloaderService getService() {
return DownloaderService.this;
}
public DownloadInfo getDownloadInfo(){
return downloadInfo;
}
public void addActivity(Activity activity, ArrayList<IDownloaderListener> callbacks) {
if(!activities.containsKey(activity)){
activities.put(activity, callbacks);
}
}
public void removeActivity(Activity activity) {
activities.remove(activity);
}
public void updateQueue(ArrayList<Video> queue){
try{
for (int i = 0; i < queue.size(); i++){
downloadQueue.add((Video) queue.get(i).clone());
}
if (downloadQueue.size() == queue.size()){
downloadInfo.setDownloadQueue(downloadQueue);
startDownload(downloadQueue);
}else{
downloadInfo.setDownloadQueue(downloadQueue);
Toast.makeText(context, getString(R.string.added_to_queue), Toast.LENGTH_SHORT).show();
}
}catch (Exception e){
Toast.makeText(context, "Couldn't update download queue! :(", Toast.LENGTH_SHORT).show();
}
}
public void cancelDownload(boolean cancelAll){
try{
YoutubeDL.getInstance().destroyProcessById(downloadProcessID);
compositeDisposable.clear();
//stopForeground(true);
if (cancelAll) {
onDownloadCancelAll();
}
}catch(Exception err){
Log.e(TAG, err.getMessage());
}
}
public void removeItemFromDownloadQueue(Video video, String type){
//if its the same video with the same download type as the current downloading one
if (downloadInfo.getVideo().getURL().equals(video.getURL()) && downloadInfo.getVideo().getDownloadedType().equals(video.getDownloadedType())){
cancelDownload(false);
downloadInfo.setDownloadType(type);
onDownloadCancel(downloadInfo);
downloadQueue.pop();
downloadInfo.setDownloadQueue(downloadQueue);
startDownload(downloadQueue);
}else {
downloadQueue.remove(video);
try{
DownloadInfo info = new DownloadInfo();
info.setVideo(video);
info.setDownloadType(type);
onDownloadCancel(info);
}catch(Exception ignored){}
}
Toast.makeText(context, video.getTitle() + " " + getString(R.string.removed_from_queue), Toast.LENGTH_SHORT).show();
}
}
private void finishService(){
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadServiceEnd();
}
});
}
}catch (Exception ignored){}
}
private void onDownloadCancelAll(){
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadCancelAll(downloadInfo);
}
});
}
}catch (Exception ignored){}
onDestroy();
}
private void onDownloadEnd(){
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadEnd(downloadInfo);
}
});
}
}catch (Exception e){
e.printStackTrace();
}
}
private void onDownloadCancel(DownloadInfo cancelInfo){
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadCancel(cancelInfo);
}
});
}
}catch (Exception ignored){}
}
private void startDownload(LinkedList<Video> videos) {
Video video;
if(videos.size() == 0){
finishService();
return;
}
try {
video = videos.peek();
} catch (Exception e) {
finishService();
return;
}
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadStart(downloadInfo);
}
});
}
}catch (Exception err){
err.printStackTrace();
}
String url = video.getURL();
YoutubeDLRequest request = new YoutubeDLRequest(url);
String type = video.getDownloadedType();
File youtubeDLDir = getDownloadLocation(type);
SharedPreferences sharedPreferences = context.getSharedPreferences("root_preferences", Activity.MODE_PRIVATE);
boolean aria2 = sharedPreferences.getBoolean("aria2", false);
if(aria2){
request.addOption("--downloader", "libaria2c.so");
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"");
}else{
int concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1);
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments);
}
String limitRate = sharedPreferences.getString("limit_rate", "");
if(!limitRate.equals("")) request.addOption("-r", limitRate);
boolean writeThumbnail = sharedPreferences.getBoolean("write_thumbnail", false);
if(writeThumbnail) {
request.addOption("--write-thumbnail");
request.addOption("--convert-thumbnails", "png");
}
request.addOption("--no-mtime");
String sponsorBlockFilters = sharedPreferences.getString("sponsorblock_filter", "");
if (!sponsorBlockFilters.isEmpty()){
request.addOption("--sponsorblock-remove", sponsorBlockFilters);
}
if (type.equals("audio")) {
request.addOption("-x");
String format = video.getAudioFormat();
if (format == null) format = sharedPreferences.getString("audio_format", "");
request.addOption("--audio-format", format);
request.addOption("--embed-metadata");
if(format.equals("mp3") || format.equals("m4a") || format.equals("flac")){
boolean embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false);
if(embedThumb){
request.addOption("--embed-thumbnail");
request.addOption("--convert-thumbnails", "png");
try{
File config = new File(getCacheDir(), "config.txt");
String config_data = "--ppa \"ffmpeg: -c:v png -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"";
FileOutputStream stream = new FileOutputStream(config);
stream.write(config_data.getBytes());
stream.close();
request.addOption("--config", config.getAbsolutePath());
}catch(Exception ignored){}
}
}
request.addCommands(Arrays.asList("--replace-in-metadata","title",".*.",video.getTitle()));
request.addCommands(Arrays.asList("--replace-in-metadata","uploader",".*.",video.getAuthor()));
request.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(uploader)s - %(title)s.%(ext)s");
} else if (type.equals("video")) {
boolean addChapters = sharedPreferences.getBoolean("add_chapters", false);
if(addChapters){
request.addOption("--sponsorblock-mark", "all");
}
boolean embedSubs = sharedPreferences.getBoolean("embed_subtitles", false);
if(embedSubs){
request.addOption("--embed-subs", "");
}
String videoQuality = video.getVideoQuality();
if (videoQuality == null) videoQuality = sharedPreferences.getString("video_quality", "");
String formatArgument = "bestvideo+bestaudio/best";
if(videoQuality.equals("Worst Quality")){
formatArgument = "worst";
}else if (!videoQuality.isEmpty() && !videoQuality.equals("Best Quality")) {
formatArgument = "bestvideo[height<="+videoQuality.substring(0, videoQuality.length()-1)+"]+bestaudio/best";
}
request.addOption("-f", formatArgument);
String format = video.getVideoFormat();
if (format == null) format = sharedPreferences.getString("video_format", "");
if(!format.equals("DEFAULT")) request.addOption("--merge-output-format", format);
if(!format.equals("webm")){
boolean embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false);
if(embedThumb){
request.addOption("--embed-thumbnail");
}
}
request.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(uploader)s - %(title)s.%(ext)s");
}
Disposable disposable = Observable.fromCallable(() -> YoutubeDL.getInstance().execute(request, downloadProcessID, callback))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(youtubeDLResponse -> {
downloadInfo.setDownloadPath(youtubeDLDir.getAbsolutePath());
downloadInfo.setDownloadType(type);
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadEnd(downloadInfo);
}
});
}
}catch (Exception e){
e.printStackTrace();
}
// SCAN NEXT IN QUEUE
videos.remove();
downloadInfo.setDownloadQueue(videos);
startDownload(videos);
}, e -> {
if (BuildConfig.DEBUG) Log.e(TAG, getString(R.string.failed_download), e);
notificationUtil.updateDownloadNotification(NotificationUtil.DOWNLOAD_NOTIFICATION_ID,
getString(R.string.failed_download), 0, 0, downloadQueue.peek().getTitle());
downloadInfo.setDownloadType(type);
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadError(downloadInfo);
}
});
}
}catch (Exception err){
err.printStackTrace();
}
// SCAN NEXT IN QUEUE
videos.remove();
downloadInfo.setDownloadQueue(videos);
startDownload(videos);
});
compositeDisposable.add(disposable);
}
private void startCommandDownload(String text){
if(!text.startsWith("yt-dlp ")){
Toast.makeText(context, "Wrong input! Try Again!", Toast.LENGTH_SHORT).show();
finishService();
return;
}
text = text.substring(6).trim();
YoutubeDLRequest request = new YoutubeDLRequest(Collections.emptyList());
String commandRegex = "\"([^\"]*)\"|(\\S+)";
Matcher m = Pattern.compile(commandRegex).matcher(text);
while (m.find()) {
if (m.group(1) != null) {
request.addOption(m.group(1));
} else {
request.addOption(m.group(2));
}
}
SharedPreferences sharedPreferences = context.getSharedPreferences("root_preferences", Activity.MODE_PRIVATE);
String downloadsDir = sharedPreferences.getString("command_path", getString(R.string.command_path));
File youtubeDLDir = new File(downloadsDir);
if (!youtubeDLDir.exists()) {
boolean isDirCreated = youtubeDLDir.mkdir();
if (!isDirCreated) {
Toast.makeText(context, R.string.failed_making_directory, Toast.LENGTH_LONG).show();
}
}
request.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(title)s.%(ext)s");
Disposable disposable = Observable.fromCallable(() -> YoutubeDL.getInstance().execute(request, downloadProcessID, callback))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(youtubeDLResponse -> {
downloadInfo.setOutputLine(youtubeDLResponse.getOut());
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadEnd(downloadInfo);
callback.onDownloadServiceEnd();
}
});
}
}catch (Exception e){
e.printStackTrace();
}
}, e -> {
downloadInfo.setOutputLine(e.getMessage());
try{
for (Activity activity: activities.keySet()){
activity.runOnUiThread(() -> {
for (int i = 0; i < activities.get(activity).size(); i++){
IDownloaderListener callback = activities.get(activity).get(i);
callback.onDownloadError(downloadInfo);
callback.onDownloadServiceEnd();
}
});
}
}catch (Exception err){
err.printStackTrace();
}
});
compositeDisposable.add(disposable);
}
@NonNull
private File getDownloadLocation(String type) {
SharedPreferences sharedPreferences = context.getSharedPreferences("root_preferences", Activity.MODE_PRIVATE);
String downloadsDir;
if (type.equals("audio")) {
downloadsDir = sharedPreferences.getString("music_path", getString(R.string.music_path));
} else {
downloadsDir = sharedPreferences.getString("video_path", getString(R.string.video_path));
}
File youtubeDLDir = new File(downloadsDir);
if (!youtubeDLDir.exists()) {
boolean isDirCreated = youtubeDLDir.mkdir();
if (!isDirCreated) {
notificationUtil.updateDownloadNotification(NotificationUtil.DOWNLOAD_NOTIFICATION_ID,
getString(R.string.failed_making_directory), 0, 0, downloadQueue.peek().getTitle());
}
}
return youtubeDLDir;
}
}

View file

@ -0,0 +1,522 @@
package com.deniscerri.ytdlnis
import android.app.Activity
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.Environment
import android.os.IBinder
import android.provider.DocumentsContract
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.deniscerri.ytdlnis.database.Video
import com.deniscerri.ytdlnis.page.CustomCommandActivity
import com.deniscerri.ytdlnis.service.DownloadInfo
import com.deniscerri.ytdlnis.service.IDownloaderListener
import com.deniscerri.ytdlnis.service.IDownloaderService
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.DownloadProgressCallback
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import com.yausername.youtubedl_android.YoutubeDLResponse
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
class DownloaderService : Service() {
private val binder = LocalBinder()
private val activities: MutableMap<Activity, ArrayList<IDownloaderListener>?> =
ConcurrentHashMap()
private val fileUtil = FileUtil()
private val downloadInfo = DownloadInfo()
private var downloadQueue = LinkedList<Video>()
private val compositeDisposable = CompositeDisposable()
private val notificationUtil = App.notificationUtil
private var context: Context? = null
var downloadProcessID = "processID"
private var downloadNotificationID = 0
private val callback =
DownloadProgressCallback { progress: Float, etaInSeconds: Long, line: String? ->
downloadInfo.progress = progress.toInt()
downloadInfo.outputLine = line
downloadInfo.downloadQueue = downloadQueue
var title: String? = getString(R.string.running_ytdlp_command)
if (!downloadQueue.isEmpty()) {
title = downloadQueue.peek()?.title
}
notificationUtil.updateDownloadNotification(
downloadNotificationID,
line!!, progress.toInt(), downloadQueue.size, title
)
try {
for (activity in activities.keys) {
activity.runOnUiThread {
if (activities[activity] != null) {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadProgress(downloadInfo)
}
}
}
}
} catch (ignored: Exception) {
}
}
override fun onCreate() {
super.onCreate()
context = this
}
override fun onBind(intent: Intent): IBinder? {
val theIntent: Intent
val pendingIntent: PendingIntent
if (intent.getBooleanExtra("rebind", false)) {
return binder
}
val id = intent.getIntExtra("id", 1)
when (id) {
NotificationUtil.DOWNLOAD_NOTIFICATION_ID -> {
theIntent = Intent(this, MainActivity::class.java)
pendingIntent =
PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE)
downloadNotificationID = id
val queue: ArrayList<out Video>? = intent.getParcelableArrayListExtra("queue")
downloadQueue = LinkedList()
downloadQueue.addAll(queue!!)
downloadInfo.downloadQueue = downloadQueue
val title = downloadInfo.video.title
val notification =
App.notificationUtil.createDownloadServiceNotification(pendingIntent, title)
startForeground(downloadNotificationID, notification)
startDownload(downloadQueue)
}
NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID -> {
theIntent = Intent(this, CustomCommandActivity::class.java)
pendingIntent =
PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE)
downloadNotificationID = id
val command = intent.getStringExtra("command")
val command_notification = App.notificationUtil.createDownloadServiceNotification(
pendingIntent,
getString(R.string.running_ytdlp_command)
)
startForeground(downloadNotificationID, command_notification)
startCommandDownload(command)
}
}
return binder
}
override fun onDestroy() {
super.onDestroy()
stopForeground(true)
stopSelf()
}
inner class LocalBinder : Binder(), IDownloaderService {
val service: DownloaderService
get() = this@DownloaderService
override fun getInfo(): DownloadInfo {
return downloadInfo
}
override fun addActivity(activity: Activity, callbacks: ArrayList<IDownloaderListener>) {
if (!activities.containsKey(activity)) {
activities[activity] = callbacks
}
}
override fun removeActivity(activity: Activity) {
activities.remove(activity)
}
override fun updateQueue(queue: ArrayList<Video>) {
try {
for (i in queue.indices) {
downloadQueue.add(queue[i].clone() as Video)
}
if (downloadQueue.size == queue.size) {
downloadInfo.downloadQueue = downloadQueue
startDownload(downloadQueue)
} else {
downloadInfo.downloadQueue = downloadQueue
Toast.makeText(context, getString(R.string.added_to_queue), Toast.LENGTH_SHORT)
.show()
}
} catch (e: Exception) {
Toast.makeText(context, "Couldn't update download queue! :(", Toast.LENGTH_SHORT)
.show()
}
}
override fun cancelDownload(cancelAll: Boolean) {
try {
YoutubeDL.getInstance().destroyProcessById(downloadProcessID)
compositeDisposable.clear()
//stopForeground(true);
if (cancelAll) {
onDownloadCancelAll()
}
} catch (err: Exception) {
Log.e(TAG, err.message!!)
}
}
override fun removeItemFromDownloadQueue(video: Video, type: String) {
//if its the same video with the same download type as the current downloading one
if (downloadInfo.video.getURL() == video.getURL() && downloadInfo.video.downloadedType == video.downloadedType) {
cancelDownload(false)
downloadInfo.downloadType = type
onDownloadCancel(downloadInfo)
downloadQueue.pop()
downloadInfo.downloadQueue = downloadQueue
startDownload(downloadQueue)
} else {
downloadQueue.remove(video)
try {
val info = DownloadInfo()
info.video = video
info.downloadType = type
onDownloadCancel(info)
} catch (ignored: Exception) {
}
}
Toast.makeText(
context,
video.title + " " + getString(R.string.removed_from_queue),
Toast.LENGTH_SHORT
).show()
}
}
private fun finishService() {
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadServiceEnd()
}
}
}
} catch (ignored: Exception) {
}
}
private fun onDownloadCancelAll() {
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadCancelAll(downloadInfo)
}
}
}
} catch (ignored: Exception) {
}
onDestroy()
}
private fun onDownloadEnd() {
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadEnd(downloadInfo)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun onDownloadCancel(cancelInfo: DownloadInfo) {
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadCancel(cancelInfo)
}
}
}
} catch (ignored: Exception) {
}
}
private fun startDownload(videos: LinkedList<Video>) {
if (videos.size == 0) {
finishService()
return
}
val video: Video = try {
videos.peek() as Video
} catch (e: Exception) {
finishService()
return
}
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadStart(downloadInfo)
}
}
}
} catch (err: Exception) {
err.printStackTrace()
}
val url = video.getURL()
val request = YoutubeDLRequest(url)
val type = video.downloadedType
val sharedPreferences = context!!.getSharedPreferences("root_preferences", 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.getString("sponsorblock_filter", "")
if (!sponsorBlockFilters!!.isEmpty()) {
request.addOption("--sponsorblock-remove", sponsorBlockFilters)
}
if (type == "audio") {
request.addOption("-x")
var format = video.audioFormat
if (format == null) format = sharedPreferences.getString("audio_format", "")
request.addOption("--audio-format", format!!)
request.addOption("--embed-metadata")
if (format == "mp3" || format == "m4a" || format == "flac") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) {
request.addOption("--embed-thumbnail")
request.addOption("--convert-thumbnails", "png")
try {
val config = File(cacheDir, "config.txt")
val config_data =
"--ppa \"ffmpeg: -c:v png -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
val stream = FileOutputStream(config)
stream.write(config_data.toByteArray())
stream.close()
request.addOption("--config", config.absolutePath)
} catch (ignored: Exception) {
}
}
}
request.addCommands(Arrays.asList("--replace-in-metadata", "title", ".*.", video.title))
request.addCommands(
Arrays.asList(
"--replace-in-metadata",
"uploader",
".*.",
video.author
)
)
request.addOption("-o", filesDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s")
} else if (type == "video") {
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 videoQuality = video.videoQuality
if (videoQuality == null) videoQuality =
sharedPreferences.getString("video_quality", "")
var formatArgument = "bestvideo+bestaudio/best"
if (videoQuality == "Worst Quality") {
formatArgument = "worst"
} else if (!videoQuality!!.isEmpty() && videoQuality != "Best Quality") {
formatArgument = "bestvideo[height<=" + videoQuality.substring(
0,
videoQuality.length - 1
) + "]+bestaudio/best"
}
request.addOption("-f", formatArgument)
var format = video.videoFormat
if (format == null) 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", filesDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s")
}
val disposable = Observable.fromCallable {
YoutubeDL.getInstance().execute(request, downloadProcessID, callback)
}
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
val destDir = Uri.parse(getDownloadLocation(type)).run {
DocumentsContract.buildChildDocumentsUriUsingTree(this, DocumentsContract.getTreeDocumentId(this))
}
fileUtil.moveFile(filesDir, context!!, destDir)
downloadInfo.downloadPath = fileUtil.formatPath(getDownloadLocation(type)!!)
Log.e(TAG, downloadInfo.downloadPath)
downloadInfo.downloadType = type
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadEnd(downloadInfo)
}
}
}
} catch (e: Exception) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
e.printStackTrace()
}
// SCAN NEXT IN QUEUE
videos.remove()
downloadInfo.downloadQueue = videos
startDownload(videos)
}) { e: Throwable? ->
if (BuildConfig.DEBUG) Log.e(TAG, getString(R.string.failed_download), e)
notificationUtil.updateDownloadNotification(
NotificationUtil.DOWNLOAD_NOTIFICATION_ID,
getString(R.string.failed_download), 0, 0, downloadQueue.peek()?.title
)
downloadInfo.downloadType = type
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadError(downloadInfo)
}
}
}
} catch (err: Exception) {
err.printStackTrace()
}
// SCAN NEXT IN QUEUE
videos.remove()
downloadInfo.downloadQueue = videos
startDownload(videos)
}
compositeDisposable.add(disposable)
}
private fun startCommandDownload(text: String?) {
var text = text
if (!text!!.startsWith("yt-dlp ")) {
Toast.makeText(context, "Wrong input! Try Again!", Toast.LENGTH_SHORT).show()
finishService()
return
}
text = text.substring(6).trim { it <= ' ' }
val request = YoutubeDLRequest(emptyList())
val commandRegex = "\"([^\"]*)\"|(\\S+)"
val m = Pattern.compile(commandRegex).matcher(text)
while (m.find()) {
if (m.group(1) != null) {
request.addOption(m.group(1))
} else {
request.addOption(m.group(2))
}
}
val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE)
request.addOption("-o", filesDir.absolutePath + "/%(title)s.%(ext)s")
val disposable = Observable.fromCallable {
YoutubeDL.getInstance().execute(request, downloadProcessID, callback)
}
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ youtubeDLResponse: YoutubeDLResponse ->
downloadInfo.outputLine = youtubeDLResponse.out
val downloadsDir = sharedPreferences.getString("command_path", getString(R.string.command_path))
val destDir = Uri.parse(downloadsDir).run {
DocumentsContract.buildChildDocumentsUriUsingTree(this, DocumentsContract.getTreeDocumentId(this))
}
fileUtil.moveFile(filesDir, context!!, destDir)
downloadInfo.outputLine += "\n\nMoved File to ${fileUtil.formatPath(downloadsDir!!)}"
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadEnd(downloadInfo)
callback.onDownloadServiceEnd()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}) { e: Throwable ->
downloadInfo.outputLine = e.message
try {
for (activity in activities.keys) {
activity.runOnUiThread {
for (i in activities[activity]!!.indices) {
val callback = activities[activity]!![i]
callback.onDownloadError(downloadInfo)
callback.onDownloadServiceEnd()
}
}
}
} catch (err: Exception) {
err.printStackTrace()
}
}
compositeDisposable.add(disposable)
}
private fun getDownloadLocation(type: String): String? {
val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE)
var downloadsDir: String? = if (type == "audio") {
sharedPreferences.getString("music_path", getString(R.string.music_path))
} else {
sharedPreferences.getString("video_path", getString(R.string.video_path))
}
Log.e(TAG, downloadsDir!!)
return downloadsDir
}
companion object {
private const val TAG = "DownloaderService"
}
}

View file

@ -59,7 +59,7 @@ class MainActivity : AppCompatActivity() {
iDownloaderService!!.addActivity(this@MainActivity, listeners)
for (i in listeners.indices) {
val listener = listeners[i]
listener.onDownloadStart(iDownloaderService!!.downloadInfo)
listener.onDownloadStart(iDownloaderService!!.info)
}
} catch (e: Exception) {
e.printStackTrace()

View file

@ -53,7 +53,7 @@ public class CustomCommandActivity extends AppCompatActivity {
ArrayList<IDownloaderListener> listeners = new ArrayList<>();
listeners.add(listener);
iDownloaderService.addActivity(CustomCommandActivity.this, listeners);
listener.onDownloadStart(iDownloaderService.getDownloadInfo());
listener.onDownloadStart(iDownloaderService.getInfo());
}catch(Exception e){
e.printStackTrace();
}

View file

@ -33,6 +33,7 @@ import com.deniscerri.ytdlnis.database.DatabaseManager;
import com.deniscerri.ytdlnis.database.Video;
import com.deniscerri.ytdlnis.service.DownloadInfo;
import com.deniscerri.ytdlnis.service.IDownloaderListener;
import com.deniscerri.ytdlnis.util.FileUtil;
import com.facebook.shimmer.ShimmerFrameLayout;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar;
@ -76,6 +77,7 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
public ArrayList<Video> selectedObjects;
private LinearProgressIndicator progressBar;
private ExtendedFloatingActionButton deleteFab;
private FileUtil fileUtil;
private String format = "";
private String website = "";
private String sort = "DESC";
@ -135,27 +137,12 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
Video item = downloadInfo.getVideo();
String url = item.getURL();
String type = downloadInfo.getDownloadType();
String path = downloadInfo.getDownloadPath();
item = findVideo(url, type);
int index = downloadsObjects.indexOf(item);
try{
// MEDIA SCAN
ArrayList<File> files = new ArrayList<>();
String title = downloadInfo.getVideo().getTitle().replaceAll("[^a-zA-Z0-9]","");
String pathTmp = "";
File path = new File(downloadInfo.getDownloadPath());
for( File file : path.listFiles() ){
if(file.isFile()){
pathTmp = file.getAbsolutePath().replaceAll("[^a-zA-Z0-9]","");
if (pathTmp.contains(title)){
files.add(file);
}
}
}
String[] paths = new String[files.size()];
for (int i = 0; i < files.size(); i++) paths[i] = files.get(i).getAbsolutePath();
MediaScannerConnection.scanFile(context, paths, null, null);
item.setDownloadedType(type);
item.setDownloadPath(path);
Calendar cal = Calendar.getInstance();
Date date = new Date();
cal.setTime(date);
@ -169,7 +156,7 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
String downloadPath = "";
try{
downloadPath = paths[0];
downloadPath = fileUtil.scanMedia(item, context);
}catch(Exception e){
e.printStackTrace();
}
@ -181,7 +168,7 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
item.setQueuedDownload(false);
databaseManager.updateHistoryItem(item);
databaseManager.close();
downloadsRecyclerViewAdapter.notifyItemChanged(downloadsObjects.indexOf(item));
downloadsRecyclerViewAdapter.notifyItemChanged(index);
} catch (Exception ignored) {}
downloading = false;
}catch(Exception ignored){}
@ -256,6 +243,7 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
selectionChips = fragmentView.findViewById(R.id.downloads_selection_chips);
websiteGroup = fragmentView.findViewById(R.id.website_chip_group);
deleteFab = fragmentView.findViewById(R.id.delete_selected_fab);
fileUtil = new FileUtil();
deleteFab.setTag("deleteSelected");
deleteFab.setOnClickListener(this);

View file

@ -1,359 +0,0 @@
package com.deniscerri.ytdlnis.page.settings;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SeekBarPreference;
import androidx.preference.SwitchPreferenceCompat;
import com.deniscerri.ytdlnis.BuildConfig;
import com.deniscerri.ytdlnis.R;
import com.deniscerri.ytdlnis.util.UpdateUtil;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
public class SettingsFragment extends PreferenceFragmentCompat {
Preference musicPath;
Preference videoPath;
Preference commandPath;
SwitchPreferenceCompat incognito;
SwitchPreferenceCompat downloadCard;
EditTextPreference apiKey;
SeekBarPreference concurrentFragments;
EditTextPreference limitRate;
SwitchPreferenceCompat aria2;
MultiSelectListPreference sponsorblockFilters;
SwitchPreferenceCompat embedSubtitles;
SwitchPreferenceCompat embedThumbnail;
SwitchPreferenceCompat addChapters;
SwitchPreferenceCompat writeThumbnail;
ListPreference audioFormat;
ListPreference videoFormat;
SeekBarPreference audioQuality;
ListPreference videoQuality;
Preference updateYTDL;
SwitchPreferenceCompat updateApp;
Preference version;
private UpdateUtil updateUtil;
public static final int MUSIC_PATH_CODE = 33333;
public static final int VIDEO_PATH_CODE = 55555;
public static final int COMMAND_PATH_CODE = 77777;
private static final String TAG = "SettingsFragment";
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
updateUtil = new UpdateUtil(requireContext());
initPreferences();
initListeners();
}
private void initPreferences(){
SharedPreferences preferences = requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
musicPath = findPreference("music_path");
videoPath = findPreference("video_path");
commandPath = findPreference("command_path");
incognito = findPreference("incognito");
downloadCard = findPreference("download_card");
apiKey = findPreference("api_key");
concurrentFragments = findPreference("concurrent_fragments");
limitRate = findPreference("limit_rate");
aria2 = findPreference("aria2");
sponsorblockFilters = findPreference("sponsorblock_filter");
embedSubtitles = findPreference("embed_subtitles");
embedThumbnail = findPreference("embed_thumbnail");
addChapters = findPreference("add_chapters");
writeThumbnail = findPreference("write_thumbnail");
audioFormat = findPreference("audio_format");
videoFormat = findPreference("video_format");
audioQuality = findPreference("audio_quality");
videoQuality = findPreference("video_quality");
updateYTDL = findPreference("update_ytdl");
updateApp = findPreference("update_app");
version = findPreference("version");
version.setSummary(BuildConfig.VERSION_NAME);
if (preferences.getString("music_path", "").isEmpty()){
editor.putString("music_path", getString(R.string.music_path));
}
if (preferences.getString("video_path", "").isEmpty()){
editor.putString("video_path", getString(R.string.video_path));
}
if (preferences.getString("command_path", "").isEmpty()){
editor.putString("command_path", getString(R.string.command_path));
}
editor.putBoolean("incognito", incognito.isChecked());
editor.putBoolean("download_card", downloadCard.isChecked());
editor.putString("api_key", apiKey.getText());
editor.putInt("concurrent_fragments", concurrentFragments.getValue());
editor.putString("limit_rate", limitRate.getText());
editor.putBoolean("aria2", aria2.isChecked());
editor.putString("sponsorblock_filter", String.join(",", sponsorblockFilters.getValues()));
editor.putBoolean("embed_subtitles", embedSubtitles.isChecked());
editor.putBoolean("embed_thumbnail", embedThumbnail.isChecked());
editor.putBoolean("add_chapters", addChapters.isChecked());
editor.putBoolean("write_thumbnail", writeThumbnail.isChecked());
editor.putString("audio_format", audioFormat.getValue());
editor.putString("video_format", videoFormat.getValue());
editor.putInt("audio_quality", audioQuality.getValue());
editor.putString("video_quality", videoQuality.getValue());
editor.putBoolean("update_app", updateApp.isChecked());
editor.apply();
}
private void initListeners(){
SharedPreferences preferences = requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
musicPath.setSummary(preferences.getString("music_path", ""));
musicPath.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
musicPathResultLauncher.launch(intent);
return true;
});
videoPath.setSummary(preferences.getString("video_path", ""));
videoPath.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
videoPathResultLauncher.launch(intent);
return true;
});
commandPath.setSummary(preferences.getString("command_path", ""));
commandPath.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
commandPathResultLauncher.launch(intent);
return true;
});
incognito.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enable = (Boolean) newValue;
editor.putBoolean("incognito", enable);
editor.apply();
return true;
});
downloadCard.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enable = (Boolean) newValue;
editor.putBoolean("download_card", enable);
editor.apply();
return true;
});
apiKey.setOnPreferenceChangeListener((preference, newValue) -> {
editor.putString("api_key", String.valueOf(newValue));
editor.apply();
return true;
});
concurrentFragments.setOnPreferenceChangeListener((preference, newValue) -> {
int value = Integer.parseInt(String.valueOf(newValue));
editor.putInt("concurrent_fragments", value);
editor.apply();
return true;
});
limitRate.setOnPreferenceChangeListener((preference, newValue) -> {
editor.putString("limit_rate", String.valueOf(newValue));
editor.apply();
return true;
});
aria2.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enable = (Boolean) newValue;
editor.putBoolean("aria2", enable);
editor.apply();
return true;
});
sponsorblockFilters.setOnPreferenceChangeListener((preference, newValue) -> {
sponsorblockFilters.setValues((Set<String>) newValue);
editor.putString("sponsorblock_filter", String.join(",", (Set<String>)newValue));
editor.apply();
return true;
});
embedSubtitles.setOnPreferenceChangeListener((preference, newValue) -> {
boolean embed = (Boolean) newValue;
editor.putBoolean("embed_subtitles", embed);
editor.apply();
return true;
});
embedThumbnail.setOnPreferenceChangeListener((preference, newValue) -> {
boolean embed = (Boolean) newValue;
editor.putBoolean("embed_thumbnail", embed);
editor.apply();
return true;
});
addChapters.setOnPreferenceChangeListener((preference, newValue) -> {
boolean add = (Boolean) newValue;
editor.putBoolean("add_chapters", add);
editor.apply();
return true;
});
writeThumbnail.setOnPreferenceChangeListener((preference, newValue) -> {
boolean write = (Boolean) newValue;
editor.putBoolean("write_thumbnail", write);
editor.apply();
return true;
});
audioFormat.setSummary(preferences.getString("audio_format", ""));
audioFormat.setOnPreferenceChangeListener((preference, newValue) -> {
editor.putString("audio_format", String.valueOf(newValue));
audioFormat.setSummary(String.valueOf(newValue));
editor.apply();
return true;
});
videoFormat.setSummary(preferences.getString("video_format", ""));
videoFormat.setOnPreferenceChangeListener((preference, newValue) -> {
editor.putString("video_format", String.valueOf(newValue));
videoFormat.setSummary(String.valueOf(newValue));
editor.apply();
return true;
});
audioQuality.setOnPreferenceChangeListener((preference, newValue) -> {
editor.putInt("audio_format", Integer.parseInt(String.valueOf(newValue)));
editor.apply();
return true;
});
videoQuality.setSummary(preferences.getString("video_quality", ""));
videoQuality.setOnPreferenceChangeListener((preference, newValue) -> {
editor.putString("video_quality", String.valueOf(newValue));
videoQuality.setSummary(String.valueOf(newValue));
editor.apply();
return true;
});
updateYTDL.setOnPreferenceClickListener(preference -> {
updateUtil.updateYoutubeDL();
return true;
});
updateApp.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enable = (Boolean) newValue;
editor.putBoolean("update_app", enable);
editor.apply();
return true;
});
version.setOnPreferenceClickListener(preference -> {
if(!updateUtil.updateApp()){
Toast.makeText(getContext(), R.string.you_are_in_latest_version, Toast.LENGTH_SHORT).show();
}
return true;
});
}
ActivityResultLauncher<Intent> musicPathResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
changePath(musicPath, result.getData(), MUSIC_PATH_CODE);
}
}
});
ActivityResultLauncher<Intent> videoPathResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
changePath(videoPath, result.getData(), VIDEO_PATH_CODE);
}
}
});
ActivityResultLauncher<Intent> commandPathResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
changePath(commandPath, result.getData(), COMMAND_PATH_CODE);
}
}
});
public void changePath(Preference p, Intent data, int requestCode){
Log.e("TEST", data.toUri(0));
String dataValue = data.getData().toString();
dataValue = dataValue.replace("content://com.android.externalstorage.documents/tree/", "");
dataValue = dataValue.replaceAll("%3A", "/");
try{
dataValue = java.net.URLDecoder.decode(dataValue, StandardCharsets.UTF_8.name());
}catch(Exception ignored){}
String[] pieces = dataValue.split("/");
int index = 1;
StringBuilder path = new StringBuilder("/storage/");
if(pieces[0].equals("primary")){
path.append("emulated/0/");
}else{
path.append(pieces[0]).append("/");
}
for(int i = index; i < pieces.length; i++){
path.append(pieces[i]).append("/");
}
Log.e(TAG, path.toString());
p.setSummary(path.toString());
SharedPreferences sharedPreferences = requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
switch (requestCode){
case MUSIC_PATH_CODE:
editor.putString("music_path", path.toString());
break;
case VIDEO_PATH_CODE:
editor.putString("video_path", path.toString());
break;
case COMMAND_PATH_CODE:
editor.putString("command_path", path.toString());
break;
}
editor.apply();
}
}

View file

@ -0,0 +1,329 @@
package com.deniscerri.ytdlnis.page.settings
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.*
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
class SettingsFragment : PreferenceFragmentCompat() {
private var musicPath: Preference? = null
private var videoPath: Preference? = null
private var commandPath: Preference? = null
private var incognito: SwitchPreferenceCompat? = null
private var downloadCard: SwitchPreferenceCompat? = null
private var apiKey: EditTextPreference? = null
private var concurrentFragments: SeekBarPreference? = null
private var limitRate: EditTextPreference? = null
private var aria2: SwitchPreferenceCompat? = null
private var sponsorblockFilters: MultiSelectListPreference? = null
private var embedSubtitles: SwitchPreferenceCompat? = null
private var embedThumbnail: SwitchPreferenceCompat? = null
private var addChapters: SwitchPreferenceCompat? = null
private var writeThumbnail: SwitchPreferenceCompat? = null
private var audioFormat: ListPreference? = null
private var videoFormat: ListPreference? = null
private var audioQuality: SeekBarPreference? = null
private var videoQuality: ListPreference? = null
private var updateYTDL: Preference? = null
private var updateApp: SwitchPreferenceCompat? = null
private var version: Preference? = null
private var updateUtil: UpdateUtil? = null
private var fileUtil: FileUtil? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
updateUtil = UpdateUtil(requireContext())
fileUtil = FileUtil()
initPreferences()
initListeners()
}
private fun initPreferences() {
val preferences =
requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE)
val editor = preferences.edit()
musicPath = findPreference("music_path")
videoPath = findPreference("video_path")
commandPath = findPreference("command_path")
incognito = findPreference("incognito")
downloadCard = findPreference("download_card")
apiKey = findPreference("api_key")
concurrentFragments = findPreference("concurrent_fragments")
limitRate = findPreference("limit_rate")
aria2 = findPreference("aria2")
sponsorblockFilters = findPreference("sponsorblock_filter")
embedSubtitles = findPreference("embed_subtitles")
embedThumbnail = findPreference("embed_thumbnail")
addChapters = findPreference("add_chapters")
writeThumbnail = findPreference("write_thumbnail")
audioFormat = findPreference("audio_format")
videoFormat = findPreference("video_format")
audioQuality = findPreference("audio_quality")
videoQuality = findPreference("video_quality")
updateYTDL = findPreference("update_ytdl")
updateApp = findPreference("update_app")
version = findPreference("version")
version!!.summary = BuildConfig.VERSION_NAME
if (preferences.getString("music_path", "")!!.isEmpty()) {
editor.putString("music_path", getString(R.string.music_path))
}
if (preferences.getString("video_path", "")!!.isEmpty()) {
editor.putString("video_path", getString(R.string.video_path))
}
if (preferences.getString("command_path", "")!!.isEmpty()) {
editor.putString("command_path", getString(R.string.command_path))
}
editor.putBoolean("incognito", incognito!!.isChecked)
editor.putBoolean("download_card", downloadCard!!.isChecked)
editor.putString("api_key", apiKey!!.text)
editor.putInt("concurrent_fragments", concurrentFragments!!.value)
editor.putString("limit_rate", limitRate!!.text)
editor.putBoolean("aria2", aria2!!.isChecked)
editor.putString(
"sponsorblock_filter",
java.lang.String.join(",", sponsorblockFilters!!.values)
)
editor.putBoolean("embed_subtitles", embedSubtitles!!.isChecked)
editor.putBoolean("embed_thumbnail", embedThumbnail!!.isChecked)
editor.putBoolean("add_chapters", addChapters!!.isChecked)
editor.putBoolean("write_thumbnail", writeThumbnail!!.isChecked)
editor.putString("audio_format", audioFormat!!.value)
editor.putString("video_format", videoFormat!!.value)
editor.putInt("audio_quality", audioQuality!!.value)
editor.putString("video_quality", videoQuality!!.value)
editor.putBoolean("update_app", updateApp!!.isChecked)
editor.apply()
}
private fun initListeners() {
val preferences =
requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE)
val editor = preferences.edit()
musicPath!!.summary = fileUtil?.formatPath(preferences.getString("music_path", "")!!)
musicPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
musicPathResultLauncher.launch(intent)
true
}
videoPath!!.summary = fileUtil?.formatPath(preferences.getString("video_path", "")!!)
videoPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
videoPathResultLauncher.launch(intent)
true
}
commandPath!!.summary = fileUtil?.formatPath(preferences.getString("command_path", "")!!)
commandPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
commandPathResultLauncher.launch(intent)
true
}
incognito!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val enable = newValue as Boolean
editor.putBoolean("incognito", enable)
editor.apply()
true
}
downloadCard!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val enable = newValue as Boolean
editor.putBoolean("download_card", enable)
editor.apply()
true
}
apiKey!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
editor.putString("api_key", newValue.toString())
editor.apply()
true
}
concurrentFragments!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val value = newValue.toString().toInt()
editor.putInt("concurrent_fragments", value)
editor.apply()
true
}
limitRate!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
editor.putString("limit_rate", newValue.toString())
editor.apply()
true
}
aria2!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val enable = newValue as Boolean
editor.putBoolean("aria2", enable)
editor.apply()
true
}
sponsorblockFilters!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
sponsorblockFilters!!.values = newValue as Set<String?>?
editor.putString("sponsorblock_filter", java.lang.String.join(",", newValue))
editor.apply()
true
}
embedSubtitles!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val embed = newValue as Boolean
editor.putBoolean("embed_subtitles", embed)
editor.apply()
true
}
embedThumbnail!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val embed = newValue as Boolean
editor.putBoolean("embed_thumbnail", embed)
editor.apply()
true
}
addChapters!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val add = newValue as Boolean
editor.putBoolean("add_chapters", add)
editor.apply()
true
}
writeThumbnail!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val write = newValue as Boolean
editor.putBoolean("write_thumbnail", write)
editor.apply()
true
}
audioFormat!!.summary = preferences.getString("audio_format", "")
audioFormat!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
editor.putString("audio_format", newValue.toString())
audioFormat!!.summary = newValue.toString()
editor.apply()
true
}
videoFormat!!.summary = preferences.getString("video_format", "")
videoFormat!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
editor.putString("video_format", newValue.toString())
videoFormat!!.summary = newValue.toString()
editor.apply()
true
}
audioQuality!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
editor.putInt("audio_format", newValue.toString().toInt())
editor.apply()
true
}
videoQuality!!.summary = preferences.getString("video_quality", "")
videoQuality!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
editor.putString("video_quality", newValue.toString())
videoQuality!!.summary = newValue.toString()
editor.apply()
true
}
updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
updateUtil!!.updateYoutubeDL()
true
}
updateApp!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val enable = newValue as Boolean
editor.putBoolean("update_app", enable)
editor.apply()
true
}
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
if (!updateUtil!!.updateApp()) {
Toast.makeText(context, R.string.you_are_in_latest_version, Toast.LENGTH_SHORT)
.show()
}
true
}
}
private var musicPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(musicPath, result.data, MUSIC_PATH_CODE)
}
}
private var videoPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(videoPath, result.data, VIDEO_PATH_CODE)
}
}
private var commandPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(commandPath, result.data, COMMAND_PATH_CODE)
}
}
private fun changePath(p: Preference?, data: Intent?, requestCode: Int) {
Log.e("TEST", data!!.toUri(0))
val path = data.data.toString()
p!!.summary = fileUtil?.formatPath(data.data.toString())
val sharedPreferences =
requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE)
val editor = sharedPreferences.edit()
when (requestCode) {
MUSIC_PATH_CODE -> editor.putString("music_path", path)
VIDEO_PATH_CODE -> editor.putString("video_path", path)
COMMAND_PATH_CODE -> editor.putString("command_path", path)
}
editor.apply()
}
companion object {
const val MUSIC_PATH_CODE = 33333
const val VIDEO_PATH_CODE = 55555
const val COMMAND_PATH_CODE = 77777
private const val TAG = "SettingsFragment"
}
}

View file

@ -5,7 +5,7 @@ import com.deniscerri.ytdlnis.database.Video;
import java.util.ArrayList;
public interface IDownloaderService {
DownloadInfo getDownloadInfo();
DownloadInfo getInfo();
void addActivity(Activity activity, ArrayList<IDownloaderListener> callback);
void removeActivity(Activity activity);
void updateQueue(ArrayList<Video> queue);

View file

@ -1,8 +1,20 @@
package com.deniscerri.ytdlnis.util
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.provider.DocumentsContract
import android.util.Log
import android.webkit.MimeTypeMap
import com.deniscerri.ytdlnis.DownloaderService
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.Video
import java.io.File
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
class FileUtil() {
class FileUtil {
fun deleteFile(path: String){
val file = File(path)
if (file.exists()) {
@ -15,4 +27,82 @@ class FileUtil {
if (path.isEmpty()) return false
return file.exists()
}
fun formatPath(path: String) : String {
var dataValue = path
dataValue = dataValue.replace("content://com.android.externalstorage.documents/tree/", "")
dataValue = dataValue.replace("%3A".toRegex(), "/")
try {
dataValue = URLDecoder.decode(dataValue, StandardCharsets.UTF_8.name())
} catch (ignored: Exception) {
}
val pieces = dataValue.split("/").toTypedArray()
var formattedPath = StringBuilder("/storage/")
if (pieces[0].equals("primary")){
formattedPath.append("emulated/0/")
}else{
formattedPath.append(pieces[0]).append("/")
}
pieces.forEachIndexed { i, it ->
if (i > 0 && it.isNotEmpty()){
formattedPath.append(it).append("/")
}
}
return formattedPath.toString()
}
fun scanMedia(video: Video, context: Context) : String {
val files = ArrayList<File>()
val title: String = video.title.replace("[^a-zA-Z0-9]".toRegex(), "")
var pathTmp: String
val path = File(video.downloadPath)
try {
for (file in path.listFiles()!!) {
if (file.isFile) {
pathTmp = file.absolutePath.replace("[^a-zA-Z0-9]".toRegex(), "")
if (pathTmp.contains(title)) {
files.add(file)
}
}
}
val paths = arrayOfNulls<String>(files.size)
for (i in files.indices) paths[i] = files[i].absolutePath
MediaScannerConnection.scanFile(context, paths, null, null)
return paths[0]!!
}catch (e: Exception){
e.printStackTrace()
}
return context.getString(R.string.unfound_file);
}
fun moveFile(originDir : File, context:Context, destDir : Uri){
originDir.listFiles().forEach {
val mimeType =
MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*"
if (it.name.equals("rList")){
it.delete()
return@forEach
}
val destUri = DocumentsContract.createDocument(
context.contentResolver,
destDir,
mimeType,
it.name
) ?: return@forEach
val inputStream = it.inputStream()
val outputStream =
context.contentResolver.openOutputStream(destUri) ?: return@forEach
inputStream.copyTo(outputStream)
inputStream.close()
outputStream.close()
it.delete()
}
}
}

View file

@ -29,28 +29,30 @@ class InfoUtil(context: Context) {
context.getSharedPreferences("root_preferences", Activity.MODE_PRIVATE)
key = sharedPreferences.getString("api_key", "")
databaseManager = DatabaseManager(context)
val thread = Thread {
//get Locale
val country = genericRequest("https://ipwho.is/")
countryCODE = try {
country.getString("country_code")
} catch (e: Exception) {
"US"
}
val res = genericRequest(invidousURL + "stats")
useInvidous = res.length() != 0
}
thread.start()
thread.join()
} catch (e: Exception) {
e.printStackTrace()
}
videos = ArrayList()
}
private fun init(){
if (countryCODE == null){
val country = genericRequest("https://ipwho.is/")
countryCODE = try {
country.getString("country_code")
} catch (e: Exception) {
"US"
}
}
if (useInvidous == false){
val res = genericRequest(invidousURL + "stats")
useInvidous = res.length() != 0
}
}
@Throws(JSONException::class)
fun search(query: String): ArrayList<Video?> {
init()
videos = ArrayList()
return if (key!!.isEmpty()) {
if (useInvidous) searchFromInvidous(query) else getFromYTDL(query)
@ -133,6 +135,7 @@ class InfoUtil(context: Context) {
@Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple {
init()
videos = ArrayList()
if (key!!.isEmpty()) {
return if (useInvidous) getPlaylistFromInvidous(id) else PlaylistTuple(
@ -216,6 +219,7 @@ class InfoUtil(context: Context) {
@Throws(JSONException::class)
fun getVideo(id: String): Video? {
init()
if (key!!.isEmpty()) {
return if (useInvidous) {
val res = genericRequest(invidousURL + "videos/" + id)
@ -392,6 +396,7 @@ class InfoUtil(context: Context) {
@Throws(JSONException::class)
fun getTrending(context: Context): ArrayList<Video?> {
init()
videos = ArrayList()
return if (key!!.isEmpty()) {
if (useInvidous) getTrendingFromInvidous(context) else ArrayList()
@ -447,6 +452,7 @@ class InfoUtil(context: Context) {
}
fun getFromYTDL(query: String): ArrayList<Video?> {
init()
videos = ArrayList()
try {
val request = YoutubeDLRequest(query)

View file

@ -30,7 +30,6 @@
app:layout_scrollFlags="scroll|enterAlways|snap"
app:title="@string/settings"
android:layout_width="match_parent"
app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/>

View file

@ -0,0 +1,7 @@
<resources>
<style name="AppTheme" parent="AppDefaultTheme">
<item name="android:windowSplashScreenAnimationDuration">0</item>
</style>
</resources>

View file

@ -148,4 +148,5 @@
<string name="sponsorblock_fillers">Fillers</string>
<string name="sponsorblock_reminders">Subscription Reminders</string>
<string name="select_sponsorblock_filtering">Select SponsorBlock Filtering</string>
<string name="unfound_file" translatable="false">/404.404</string>
</resources>

View file

@ -5,8 +5,11 @@
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:color">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
</style>
<style name="AppTheme" parent="AppDefaultTheme" />
<style name="Toolbar" parent="Theme.Material3.DayNight.NoActionBar">
<item name="searchViewStyle">@style/searchStyle</item>
</style>