This commit is contained in:
Denis Çerri 2023-01-08 16:49:24 +01:00
parent 51ff22abcd
commit 2a02175ade
No known key found for this signature in database
GPG key ID: 96B3554AF5B193EE
19 changed files with 959 additions and 1173 deletions

8
.gitignore vendored
View file

@ -1,7 +1,13 @@
*.iml
.gradle
/local.properties
/.idea
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/caches
/.idea/tasks.xml
/.idea/gradle.xml
/.idea/dictionaries
.DS_Store
/build
/captures

View file

@ -11,7 +11,7 @@
| Build | Latest Release | Downloads | Contribute |
|-------|----------|------------|------------|
| ![CI](https://github.com/deniscerri/ytdlnis/actions/workflows/android.yml/badge.svg?branch=main&event=pull) | [![stable release](https://img.shields.io/github/release/deniscerri/ytdlnis.svg?maxAge=3600&label=download)](https://github.com/deniscerri/ytdlnis/releases/latest) | [![downloads](https://img.shields.io/github/downloads/deniscerri/ytdlnis/total?style=flat-square)](https://github.com/deniscerri/ytdlnis/releases) | [![Translation status](https://hosted.weblate.org/widgets/ytdlnis/-/svg-badge.svg)](https://hosted.weblate.org/engage/ytdlnis/?utm_source=widget) |
| ![CI](https://github.com/deniscerri/ytdlnis/actions/workflows/android.yml/badge.svg?branch=main&event=pull) | [![stable release](https://img.shields.io/github/release/deniscerri/ytdlnis.svg?maxAge=3600&label=download)](https://github.com/deniscerri/ytdlnis/releases) | [![downloads](https://img.shields.io/github/downloads/deniscerri/ytdlnis/total?style=flat-square)](https://img.shields.io/github/downloads/deniscerri/ytdlnis/total?style=flat-square) | [![Translation status](https://hosted.weblate.org/widgets/ytdlnis/-/svg-badge.svg)](https://hosted.weblate.org/engage/ytdlnis/?utm_source=widget) |
</div>

View file

@ -30,7 +30,7 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
archivesBaseName = "YTDLnis-$versionName"
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
vectorDrawables {
useSupportLibrary true
@ -75,7 +75,7 @@ android {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
include 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
}
@ -91,9 +91,9 @@ dependencies {
// implementation project(":library")
// implementation project(":ffmpeg")
implementation "com.github.JunkFood02.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.JunkFood02.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.JunkFood02.youtubedl-android:aria2c:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer"
implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -119,7 +119,7 @@ dependencies {
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation "androidx.work:work-runtime-ktx:$workVer"
implementation "androidx.work:work-runtime:$workVer"
implementation "androidx.room:room-runtime:2.4.3"
implementation "androidx.room:room-ktx:2.4.3"
@ -127,8 +127,7 @@ dependencies {
androidTestImplementation "androidx.room:room-testing:2.4.3"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation "com.google.truth:truth:1.1.3"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer"
androidTestImplementation "com.google.truth:truth:1.1.3"
}

View file

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

View file

@ -0,0 +1,515 @@
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");
Set<String> sponsorBlockFilters = sharedPreferences.getStringSet("sponsorblock_filtes", Collections.emptySet());
if (!sponsorBlockFilters.isEmpty()){
request.addOption("--sponsorblock-remove", String.join(",", 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");
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 ")){
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

@ -1,592 +0,0 @@
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.Build
import android.os.IBinder
import android.os.SystemClock
import android.provider.DocumentsContract
import android.provider.Settings.Global
import android.util.Log
import android.widget.Toast
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.database.Video
import com.deniscerri.ytdlnis.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.deniscerri.ytdlnis.work.FileTransferWorker
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.originDir
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.title
import com.deniscerri.ytdlnis.work.FileTransferWorker.Companion.downLocation
import com.yausername.youtubedl_android.DownloadProgressCallback
import com.yausername.youtubedl_android.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 kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
import kotlin.jvm.functions.Function3
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 lateinit var notificationChannelID : String
private lateinit var workManager : WorkManager
private val callback: (Float, Long?, String?) -> Unit =
{ progress: Float, _: 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,
notificationChannelID
)
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
this.workManager = WorkManager.getInstance(context!!.applicationContext)
}
override fun onBind(intent: Intent): IBinder? {
val theIntent: Intent
val pendingIntent: PendingIntent
if (intent.getBooleanExtra("rebind", false)) {
return binder
}
when (val id = intent.getIntExtra("id", 1)) {
NotificationUtil.DOWNLOAD_NOTIFICATION_ID -> {
notificationChannelID = NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_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, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
startForeground(downloadNotificationID, notification)
startDownload(downloadQueue)
}
NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID -> {
notificationChannelID = NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
theIntent = Intent(this, CustomCommandActivity::class.java)
pendingIntent =
PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE)
downloadNotificationID = id
val command = intent.getStringExtra("command")
val commandNotification = App.notificationUtil.createDownloadServiceNotification(
pendingIntent,
getString(R.string.running_ytdlp_command),
NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
)
startForeground(downloadNotificationID, commandNotification)
startCommandDownload(command)
}
}
return binder
}
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
stopForeground(STOP_FOREGROUND_REMOVE)
}else{
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 downloadLocation = getDownloadLocation(type)
var tempFileDir = File(cacheDir.absolutePath + """/${video.title}##${video.downloadedType}""")
tempFileDir.delete()
tempFileDir.mkdir()
val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE)
val 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)
}
if (type == "audio") {
if (downloadLocation.equals(getString(R.string.music_path))){
tempFileDir = File(getString(R.string.music_path))
}
request.addOption("-x")
var format = video.audioFormat
if (format == null) format = sharedPreferences.getString("audio_format", "")
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(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", tempFileDir.absolutePath + "/%(uploader)s - %(title)s.%(ext)s")
} else if (type == "video") {
if (downloadLocation.equals(getString(R.string.video_path))){
tempFileDir = File(getString(R.string.video_path))
}
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
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", tempFileDir.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 workTag = video.getURL()
val workData = workDataOf(
originDir to tempFileDir.absolutePath,
downLocation to downloadLocation,
title to video.title
)
val fileTransferWorkRequest = OneTimeWorkRequestBuilder<FileTransferWorker>()
.addTag(workTag)
.setInputData(workData)
.build()
workManager.enqueueUniqueWork(
workTag,
ExistingWorkPolicy.KEEP,
fileTransferWorkRequest
)
downloadInfo.downloadPath = fileUtil.formatPath(getDownloadLocation(type)!!)
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? ->
tempFileDir.delete()
if (BuildConfig.DEBUG) {
Toast.makeText(context, e!!.message, Toast.LENGTH_LONG).show()
Log.e(TAG, getString(R.string.failed_download), e)
}
notificationUtil.updateDownloadNotification(
NotificationUtil.DOWNLOAD_NOTIFICATION_ID,
getString(R.string.failed_download), 0, 0, downloadQueue.peek()?.title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
)
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 ")) {
text = text.substring(6).trim { it <= ' ' }
}
val sharedPreferences = context!!.getSharedPreferences("root_preferences", MODE_PRIVATE)
val downloadLocation = sharedPreferences.getString("command_path", getString(R.string.command_path))
var tempFileDir = File(cacheDir.absolutePath + "/command")
tempFileDir.delete()
tempFileDir.mkdir()
if (downloadLocation.equals(getString(R.string.command_path))){
tempFileDir = File(getString(R.string.command_path))
}
val request = YoutubeDLRequest(emptyList())
val 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))
}
}
request.addOption("-o", tempFileDir.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 workTag = SystemClock.uptimeMillis().toString()
val workData = workDataOf(
originDir to tempFileDir.absolutePath,
downLocation to downloadLocation,
title to ""
)
val fileTransferWorkRequest = OneTimeWorkRequestBuilder<FileTransferWorker>()
.addTag(workTag)
.setInputData(workData)
.build()
workManager.enqueueUniqueWork(
workTag,
ExistingWorkPolicy.KEEP,
fileTransferWorkRequest
)
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
tempFileDir.delete()
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

@ -60,7 +60,7 @@ class MainActivity : AppCompatActivity() {
iDownloaderService!!.addActivity(this@MainActivity, listeners)
for (i in listeners.indices) {
val listener = listeners[i]
listener.onDownloadStart(iDownloaderService!!.info)
listener.onDownloadStart(iDownloaderService!!.downloadInfo)
}
} catch (e: Exception) {
e.printStackTrace()
@ -316,17 +316,6 @@ class MainActivity : AppCompatActivity() {
}
}
private fun createDefaultFolders(){
val audio = File(getString(R.string.music_path))
val video = File(getString(R.string.video_path))
val command = File(getString(R.string.command_path))
audio.mkdirs()
video.mkdirs()
command.mkdirs()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
@ -338,11 +327,22 @@ class MainActivity : AppCompatActivity() {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
createPermissionRequestDialog()
}else{
createDefaultFolders()
createDefaultFolders();
}
}
}
private fun createDefaultFolders(){
val audio = File(getString(R.string.music_path))
val video = File(getString(R.string.video_path))
val command = File(getString(R.string.command_path))
audio.mkdirs()
video.mkdirs()
command.mkdirs()
}
private fun exit() {
finishAffinity()
exitProcess(0)

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.getInfo());
listener.onDownloadStart(iDownloaderService.getDownloadInfo());
}catch(Exception e){
e.printStackTrace();
}
@ -76,14 +76,7 @@ public class CustomCommandActivity extends AppCompatActivity {
}
public void onDownloadProgress(DownloadInfo info) {
String newInfo = info.getOutputLine();
if (newInfo.contains("[download]")){
String temp = output.getText().toString();
output.setText(
temp.substring(0, temp.lastIndexOf(System.getProperty("line.separator")) - 2)
);
}
output.append(info.getOutputLine() + " \n");
output.append("\n" + info.getOutputLine() + "\n");
output.scrollTo(0, output.getHeight());
scrollView.fullScroll(View.FOCUS_DOWN);
}
@ -148,7 +141,6 @@ public class CustomCommandActivity extends AppCompatActivity {
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);

View file

@ -33,7 +33,6 @@ 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;
@ -77,7 +76,6 @@ 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";
@ -125,7 +123,6 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
int position = downloadsObjects.indexOf(v);
downloadsRecyclerViewAdapter.notifyItemRemoved(position);
downloadsObjects.remove(v);
if (downloadsObjects.isEmpty()) initCards();
downloading = false;
}catch(Exception e){
@ -137,12 +134,28 @@ 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);
@ -156,13 +169,12 @@ public class DownloadsFragment extends Fragment implements DownloadsRecyclerView
String downloadPath = "";
try{
downloadPath = fileUtil.scanMedia(item, context);
downloadPath = paths[0];
}catch(Exception e){
e.printStackTrace();
}
databaseManager = new DatabaseManager(context);
//TODO observe file transfer worker here and then update, but first you need to turn this to kotlin and implement proper mvvm
try {
item.setDownloadedTime(downloadedTime);
item.setDownloadPath(downloadPath);
@ -244,7 +256,6 @@ 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

@ -0,0 +1,359 @@
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.putStringSet("sponsorblock_filters", 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.putStringSet("sponsorblock_filters", (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

@ -1,326 +0,0 @@
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.putStringSet("sponsorblock_filters", 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.putStringSet("sponsorblock_filters", 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 getInfo();
DownloadInfo getDownloadInfo();
void addActivity(Activity activity, ArrayList<IDownloaderListener> callback);
void removeActivity(Activity activity);
void updateQueue(ArrayList<Video> queue);

View file

@ -1,24 +1,8 @@
package com.deniscerri.ytdlnis.util
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.FileUtils
import android.provider.DocumentsContract
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import com.deniscerri.ytdlnis.DownloaderService
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.Video
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
class FileUtil() {
class FileUtil {
fun deleteFile(path: String){
val file = File(path)
if (file.exists()) {
@ -31,96 +15,4 @@ 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, progress: (p: Int) -> Unit){
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
val fileLength = it.length()
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = inputStream.read(buffer)
try {
while (bytes >= 0) {
outputStream.write(buffer, 0, bytes)
bytesCopied += bytes
progress((bytesCopied * 100 / fileLength).toInt())
bytes = inputStream.read(buffer)
}
}catch (e : Exception){
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
inputStream.close()
outputStream.close()
it.delete()
}
originDir.delete()
}
}

View file

@ -29,30 +29,28 @@ 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)
@ -135,7 +133,6 @@ 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(
@ -219,7 +216,6 @@ 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)
@ -396,7 +392,6 @@ 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()
@ -452,7 +447,6 @@ class InfoUtil(context: Context) {
}
fun getFromYTDL(query: String): ArrayList<Video?> {
init()
videos = ArrayList()
try {
val request = YoutubeDLRequest(query)

View file

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

View file

@ -24,7 +24,6 @@ import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.atomic.AtomicReference
import java.io.File
class UpdateUtil(var context: Context) {
private val tag = "UpdateUtil"
@ -164,12 +163,11 @@ class UpdateUtil(var context: Context) {
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ status: UpdateStatus ->
when (status) {
UpdateStatus.DONE ->
Toast.makeText(
context,
context.getString(R.string.ytld_update_success),
Toast.LENGTH_LONG
).show()
UpdateStatus.DONE -> Toast.makeText(
context,
context.getString(R.string.ytld_update_success),
Toast.LENGTH_LONG
).show()
UpdateStatus.ALREADY_UP_TO_DATE -> Toast.makeText(
context,
context.getString(R.string.you_are_in_latest_version),
@ -178,7 +176,7 @@ class UpdateUtil(var context: Context) {
else -> Toast.makeText(context, status.toString(), Toast.LENGTH_LONG).show()
}
updatingYTDL = false
} as ((UpdateStatus?) -> Unit)?) { e: Throwable? ->
}) { e: Throwable? ->
if (BuildConfig.DEBUG) Log.e(tag, context.getString(R.string.ytdl_update_failed), e)
Toast.makeText(
context,

View file

@ -45,7 +45,8 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/ic_app_icon"
android:scaleType="center" />
android:scaleType="center"
/>
</LinearLayout>

View file

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

View file

@ -5,7 +5,7 @@ buildscript {
def versionMajor = 1
def versionMinor = 4
def versionPatch = 8
def versionPatch = 9
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
ext {
@ -14,18 +14,17 @@ buildscript {
// dependency versions
appCompatVer = '1.5.1'
junitVer = '4.13.2'
androidJunitVer = '1.1.1'
espressoVer = '3.2.0'
androidJunitVer = '1.1.4'
espressoVer = '3.5.0'
jacksonVer = '2.9.8'
// supports java 1.6
commonsIoVer = '2.5'
// supports java 1.6
commonsCompressVer = '1.12'
youtubedlAndroidVer = "a3b971724d"
youtubedlAndroidVer = "master-SNAPSHOT"
workVer = "2.7.1"
composeVer = "1.3.2"
kotlinVer = "1.7.20"
coroutineVer = "1.6.4"
}
repositories {
mavenCentral()