mirror of https://github.com/deniscerri/ytdlnis
added stateless downloads & more
changed folder structure to ytdlnis instead of ytdl when downloads are finished, history fragment updates itself changed video duration of livestreams to LIVE Fixed update dialog showing when you dont have internet removed download functionality from home fragment and put it all on downloadservice. This makes the download stateless and the ui can be updated when you go back to the app did the same for custom command activity slight changes on result card ui added fastlane metadata added support for other yt-dlp videos instead of just focusing on youtube. Youtube is still the main focus updated database slight design changes made bottom navigation bar the same color as the background changed download buttons background colors inside cards fixed result items changing download status icons when scrolling through recyclerview updated readmepull/1/head
parent
2e355562d3
commit
6473f452a1
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<runningDeviceTargetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="RUNNING_DEVICE_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="SERIAL_NUMBER" />
|
||||
<value value="192.168.1.131:44159" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</runningDeviceTargetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2022-09-23T20:00:37.414283600Z" />
|
||||
</component>
|
||||
</project>
|
||||
@ -1,23 +1,56 @@
|
||||
# ytdlnis
|
||||
Android App for downloading youtube videos
|
||||
|
||||
Created as a project for the class, Mobile Programming.
|
||||
<h1 align="center">
|
||||
ytldnis
|
||||
</h1>
|
||||
<p align="center">
|
||||
Android App for downloading videos using yt-dlp
|
||||
</p>
|
||||
<p align="center">
|
||||
Created as a project for the class, Mobile Programming. Continued to update it because why not.
|
||||
</p>
|
||||
|
||||
Features:
|
||||
---
|
||||
- [x] download audio
|
||||
- [x] download video
|
||||
- [x] download full playlist in audio or video format
|
||||
- [x] use custom download paths for each format
|
||||
- [x] download history
|
||||
- [x] search from the app
|
||||
- [x] works with youtube links
|
||||
- [x] share videos / playlists from the Youtube App using the share menu
|
||||
- [x] heads-up download notifications
|
||||
## Download
|
||||
|
||||
|||
|
||||
|:-------------------------:|:-------------------------:|
|
||||
||
|
||||
||
|
||||
[Get Latest Release Here](https://github.com/deniscerri/ytdlnis/releases/)
|
||||
|
||||
## Features:
|
||||
|
||||
- download audio and video files
|
||||
- download full playlists
|
||||
- download selected videos on a list and add to a queue
|
||||
- use custom download paths for each format
|
||||
- remove non music parts from youtube audio files
|
||||
- embed subtitles on videos
|
||||
- add chapters to youtube videos by using sponsorblock chapters
|
||||
- select different download formats
|
||||
- share any yt-dlp compatible link into the app and start downloading
|
||||
- foreground download services which work even if app is cleared from recents and updates ui when u go back
|
||||
- search from the app
|
||||
- material you interface
|
||||
- really easy to use
|
||||
- run terminal commands for more flexibility
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div>
|
||||
<img src="fastlane/metadata/android/en-US/images/screenshots/1.png" width="30%" />
|
||||
<img src="fastlane/metadata/android/en-US/images/screenshots/2.png" width="30%" />
|
||||
<img src="fastlane/metadata/android/en-US/images/screenshots/3.png" width="30%" />
|
||||
<img src="fastlane/metadata/android/en-US/images/screenshots/4.png" width="30%" />
|
||||
<img src="fastlane/metadata/android/en-US/images/screenshots/5.png" width="30%" />
|
||||
<img src="fastlane/metadata/android/en-US/images/screenshots/6.png" width="30%" />
|
||||
</div>
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[GNU GPL v3.0](https://github.com/deniscerri/ytdlnis/blob/main/LICENSE)
|
||||
|
||||
## Thanks
|
||||
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) and its contributors for making this tool possible. Without it this app wouldn't exist.
|
||||
- [youtubedl-android](https://github.com/yausername/youtubedl-android) for porting yt-dlp to android
|
||||
- [dvd](https://github.com/yausername/dvd) for showing how to implement youtubedl-android
|
||||
- [seal](https://github.com/JunkFood02/Seal) for certain design elements and features i wanted to use aswell in this app
|
||||
|
||||
and alot of other internet forum strangers.
|
||||
@ -1,4 +1,4 @@
|
||||
package com.deniscerri.ytdl;
|
||||
package com.deniscerri.ytdlnis;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
@ -1,47 +0,0 @@
|
||||
package com.deniscerri.ytdl;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
public class DownloaderService extends Service {
|
||||
|
||||
private LocalBinder binder = new LocalBinder();
|
||||
private NotificationCompat.Builder builder;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
Intent theIntent = new Intent(this, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
String title = intent.getStringExtra("title");
|
||||
int id = intent.getIntExtra("id", 1);
|
||||
|
||||
Notification notification = App.notificationUtil.createDownloadServiceNotification(pendingIntent,title);
|
||||
startForeground(id, notification);
|
||||
return binder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
return super.onUnbind(intent);
|
||||
}
|
||||
|
||||
public class LocalBinder extends Binder {
|
||||
|
||||
public DownloaderService getService() {
|
||||
return DownloaderService.this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,206 +0,0 @@
|
||||
package com.deniscerri.ytdl.page;
|
||||
|
||||
import static com.deniscerri.ytdl.App.notificationUtil;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import com.deniscerri.ytdl.BuildConfig;
|
||||
import com.deniscerri.ytdl.DownloaderService;
|
||||
import com.deniscerri.ytdl.R;
|
||||
import com.deniscerri.ytdl.page.settings.SettingsFragment;
|
||||
import com.deniscerri.ytdl.util.NotificationUtil;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
|
||||
import com.yausername.youtubedl_android.DownloadProgressCallback;
|
||||
import com.yausername.youtubedl_android.YoutubeDL;
|
||||
import com.yausername.youtubedl_android.YoutubeDLRequest;
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
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 CustomCommandActivity extends AppCompatActivity {
|
||||
private static final String TAG = "CustomCommandActivity";
|
||||
private MaterialToolbar topAppBar;
|
||||
private boolean running = false;
|
||||
private boolean isDownloadServiceRunning = false;
|
||||
public DownloaderService downloaderService;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private TextView output;
|
||||
private EditText input;
|
||||
private ExtendedFloatingActionButton fab;
|
||||
private ExtendedFloatingActionButton cancelFab;
|
||||
Context context;
|
||||
|
||||
private final DownloadProgressCallback callback = (progress, etaInSeconds, line) -> CustomCommandActivity.this.runOnUiThread(() -> {
|
||||
output.append("\n"+line);
|
||||
notificationUtil.updateDownloadNotification(NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID,
|
||||
line, (int) progress, 0, getString(R.string.running_ytdlp_command));
|
||||
});
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
downloaderService = ((DownloaderService.LocalBinder) service).getService();
|
||||
isDownloadServiceRunning = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
downloaderService = null;
|
||||
isDownloadServiceRunning = false;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_custom_command);
|
||||
|
||||
context = getBaseContext();
|
||||
topAppBar = findViewById(R.id.custom_command_toolbar);
|
||||
topAppBar.setNavigationOnClickListener(view -> onBackPressed());
|
||||
output = findViewById(R.id.custom_command_output);
|
||||
output.setMovementMethod(new ScrollingMovementMethod());
|
||||
|
||||
input = findViewById(R.id.command_edittext);
|
||||
input.requestFocus();
|
||||
|
||||
fab = findViewById(R.id.command_fab);
|
||||
fab.setOnClickListener(view -> {
|
||||
if (isStoragePermissionGranted()){
|
||||
runCommand(input.getText().toString());
|
||||
}
|
||||
});
|
||||
|
||||
cancelFab = findViewById(R.id.cancel_command_fab);
|
||||
cancelFab.setOnClickListener(view -> {
|
||||
compositeDisposable.clear();
|
||||
stopDownloadService();
|
||||
swapFabs();
|
||||
running = false;
|
||||
input.setEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
private void swapFabs(){
|
||||
int cancel = cancelFab.getVisibility();
|
||||
int start = fab.getVisibility();
|
||||
cancelFab.setVisibility(start);
|
||||
fab.setVisibility(cancel);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private void runCommand(String text){
|
||||
if (running) {
|
||||
Toast.makeText(this, "Cannot start command! A command is already in progress", Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
if(!text.startsWith("yt-dlp ")){
|
||||
Toast.makeText(context, "Wrong input! Try Again!", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
input.setEnabled(false);
|
||||
output.setText("");
|
||||
startDownloadService(getString(R.string.running_ytdlp_command), NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID);
|
||||
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");
|
||||
|
||||
running = true;
|
||||
swapFabs();
|
||||
|
||||
Disposable disposable = Observable.fromCallable(() -> YoutubeDL.getInstance().execute(request, callback))
|
||||
.subscribeOn(Schedulers.newThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(youtubeDLResponse -> {
|
||||
output.append(youtubeDLResponse.getOut());
|
||||
running = false;
|
||||
stopDownloadService();
|
||||
// MEDIA SCAN
|
||||
MediaScannerConnection.scanFile(context, new String[]{"/storage"}, null, null);
|
||||
input.setEnabled(true);
|
||||
swapFabs();
|
||||
}, e -> {
|
||||
if (BuildConfig.DEBUG) Log.e(TAG, getString(R.string.failed_download), e);
|
||||
output.append(e.getMessage());
|
||||
running = false;
|
||||
stopDownloadService();
|
||||
swapFabs();
|
||||
input.setEnabled(true);
|
||||
});
|
||||
compositeDisposable.add(disposable);
|
||||
}
|
||||
|
||||
public void startDownloadService(String title, int id){
|
||||
if(isDownloadServiceRunning) return;
|
||||
Intent serviceIntent = new Intent(context, DownloaderService.class);
|
||||
serviceIntent.putExtra("title", title);
|
||||
serviceIntent.putExtra("id", id);
|
||||
context.getApplicationContext().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
public void stopDownloadService(){
|
||||
if(!isDownloadServiceRunning) return;
|
||||
context.getApplicationContext().unbindService(serviceConnection);
|
||||
isDownloadServiceRunning = false;
|
||||
}
|
||||
|
||||
public boolean isStoragePermissionGranted() {
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
}else{
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,374 @@
|
||||
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.media.MediaScannerConnection;
|
||||
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.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.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
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, 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 final DownloadProgressCallback callback = (progress, etaInSeconds, line) -> {
|
||||
downloadInfo.setProgress((int) progress);
|
||||
downloadInfo.setOutputLine(line);
|
||||
downloadInfo.setDownloadQueue(downloadQueue);
|
||||
notificationUtil.updateDownloadNotification(NotificationUtil.DOWNLOAD_NOTIFICATION_ID,
|
||||
line, (int) progress, downloadQueue.size(), downloadQueue.peek().getTitle());
|
||||
|
||||
try{
|
||||
for (Activity activity: activities.keySet()){
|
||||
activity.runOnUiThread(() -> {
|
||||
IDownloaderListener callback = activities.get(activity);
|
||||
callback.onDownloadProgress(downloadInfo);
|
||||
});
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
context = this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
Intent theIntent = new Intent(this, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
if(intent.getBooleanExtra("rebind", false)){
|
||||
return binder;
|
||||
}
|
||||
|
||||
int id = intent.getIntExtra("id", 1);
|
||||
switch (id){
|
||||
case NotificationUtil.DOWNLOAD_NOTIFICATION_ID:
|
||||
ArrayList queue = (ArrayList<Video>) intent.getSerializableExtra("queue");
|
||||
downloadQueue = new LinkedList<>();
|
||||
downloadQueue.addAll(queue);
|
||||
downloadInfo.setDownloadQueue(downloadQueue);
|
||||
|
||||
String title = downloadInfo.getVideo().getTitle();
|
||||
Notification notification = App.notificationUtil.createDownloadServiceNotification(pendingIntent,title);
|
||||
startForeground(NotificationUtil.DOWNLOAD_NOTIFICATION_ID, notification);
|
||||
startDownload(downloadQueue);
|
||||
break;
|
||||
case NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID:
|
||||
String command = intent.getStringExtra("command");
|
||||
Notification command_notification = App.notificationUtil.createDownloadServiceNotification(pendingIntent,getString(R.string.running_ytdlp_command));
|
||||
startForeground(NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID, command_notification);
|
||||
startCommandDownload(command);
|
||||
break;
|
||||
}
|
||||
return binder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
return super.onUnbind(intent);
|
||||
}
|
||||
|
||||
public class LocalBinder extends Binder implements IDownloaderService {
|
||||
public DownloaderService getService() {
|
||||
return DownloaderService.this;
|
||||
}
|
||||
|
||||
public DownloadInfo getDownloadInfo(){
|
||||
return downloadInfo;
|
||||
}
|
||||
|
||||
public void addActivity(Activity activity, IDownloaderListener callback) {
|
||||
if(!activities.containsKey(activity)){
|
||||
activities.put(activity, callback);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeActivity(Activity activity) {
|
||||
activities.remove(activity);
|
||||
}
|
||||
|
||||
public void updateQueue(ArrayList<Video> queue){
|
||||
downloadQueue.addAll(queue);
|
||||
}
|
||||
|
||||
public void cancelDownload(){
|
||||
try{
|
||||
YoutubeDL.getInstance().destroyProcessById(downloadProcessID);
|
||||
}catch(Exception err){
|
||||
Log.e(TAG, err.getMessage());
|
||||
}
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void finishService(){
|
||||
try{
|
||||
for (Activity activity: activities.keySet()){
|
||||
activity.runOnUiThread(() -> {
|
||||
IDownloaderListener callback = activities.get(activity);
|
||||
callback.onDownloadServiceEnd();
|
||||
});
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void startDownload(LinkedList<Video> videos) {
|
||||
Video video;
|
||||
if(videos.size() == 0){
|
||||
finishService();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
video = videos.peek();
|
||||
} catch (Exception e) {
|
||||
finishService();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
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("--no-mtime");
|
||||
|
||||
if (type.equals("mp3")) {
|
||||
boolean removeNonMusic = sharedPreferences.getBoolean("remove_non_music", false);
|
||||
if(removeNonMusic){
|
||||
request.addOption("--sponsorblock-remove", "all");
|
||||
}
|
||||
request.addOption("--postprocessor-args", "-write_id3v1 1 -id3v2_version 3");
|
||||
request.addOption("--add-metadata");
|
||||
request.addOption("-x");
|
||||
String format = sharedPreferences.getString("audio_format", "");
|
||||
request.addOption("--audio-format", format);
|
||||
|
||||
if(format.equals("mp3") || format.equals("m4a") || format.equals("flac")){
|
||||
boolean embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false);
|
||||
if(embedThumb){
|
||||
request.addOption("--embed-thumbnail");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (type.equals("mp4")) {
|
||||
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", "");
|
||||
}
|
||||
request.addOption("-f", "bestvideo+bestaudio/best");
|
||||
String format = sharedPreferences.getString("video_format", "");
|
||||
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() + "/%(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());
|
||||
try{
|
||||
for (Activity activity: activities.keySet()){
|
||||
activity.runOnUiThread(() -> {
|
||||
IDownloaderListener callback = activities.get(activity);
|
||||
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());
|
||||
|
||||
try{
|
||||
for (Activity activity: activities.keySet()){
|
||||
activity.runOnUiThread(() -> {
|
||||
IDownloaderListener callback = activities.get(activity);
|
||||
callback.onDownloadError(downloadInfo);
|
||||
});
|
||||
}
|
||||
}catch (Exception err){
|
||||
err.printStackTrace();
|
||||
}
|
||||
|
||||
// SCAN NEXT IN QUEUE
|
||||
videos.remove();
|
||||
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(() -> {
|
||||
IDownloaderListener callback = activities.get(activity);
|
||||
callback.onDownloadEnd(downloadInfo);
|
||||
callback.onDownloadServiceEnd();
|
||||
});
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, e -> {
|
||||
downloadInfo.setOutputLine(e.getMessage());
|
||||
try{
|
||||
for (Activity activity: activities.keySet()){
|
||||
activity.runOnUiThread(() -> {
|
||||
IDownloaderListener callback = activities.get(activity);
|
||||
callback.onDownloadError(downloadInfo);
|
||||
});
|
||||
}
|
||||
}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("mp3")) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,175 @@
|
||||
package com.deniscerri.ytdlnis.page;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.deniscerri.ytdlnis.R;
|
||||
import com.deniscerri.ytdlnis.DownloaderService;
|
||||
import com.deniscerri.ytdlnis.database.Video;
|
||||
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.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
|
||||
import java.util.Date;
|
||||
|
||||
public class CustomCommandActivity extends AppCompatActivity {
|
||||
private static final String TAG = "CustomCommandActivity";
|
||||
private MaterialToolbar topAppBar;
|
||||
private boolean isDownloadServiceRunning = false;
|
||||
public DownloaderService downloaderService;
|
||||
private TextView output;
|
||||
private EditText input;
|
||||
private ExtendedFloatingActionButton fab;
|
||||
private ExtendedFloatingActionButton cancelFab;
|
||||
private IDownloaderService iDownloaderService;
|
||||
Context context;
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
downloaderService = ((DownloaderService.LocalBinder) service).getService();
|
||||
iDownloaderService = (IDownloaderService) service;
|
||||
isDownloadServiceRunning = true;
|
||||
try{
|
||||
iDownloaderService.addActivity(CustomCommandActivity.this, listener);
|
||||
listener.onDownloadStart(iDownloaderService.getDownloadInfo());
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
downloaderService = null;
|
||||
iDownloaderService = null;
|
||||
isDownloadServiceRunning = false;
|
||||
}
|
||||
};
|
||||
|
||||
public IDownloaderListener listener = new IDownloaderListener() {
|
||||
|
||||
public void onDownloadStart(DownloadInfo info) {
|
||||
input.setEnabled(false);
|
||||
output.setText("");
|
||||
swapFabs();
|
||||
}
|
||||
|
||||
public void onDownloadProgress(DownloadInfo info) {
|
||||
output.append("\n" + info.getOutputLine());
|
||||
}
|
||||
|
||||
public void onDownloadError(DownloadInfo info) {
|
||||
output.append("\n" + info.getOutputLine());
|
||||
input.setEnabled(true);
|
||||
swapFabs();
|
||||
}
|
||||
|
||||
public void onDownloadEnd(DownloadInfo info) {
|
||||
output.append(info.getOutputLine());
|
||||
// MEDIA SCAN
|
||||
MediaScannerConnection.scanFile(context, new String[]{"/storage"}, null, null);
|
||||
input.setEnabled(true);
|
||||
swapFabs();
|
||||
}
|
||||
|
||||
|
||||
public void onDownloadServiceEnd() {
|
||||
stopDownloadService();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_custom_command);
|
||||
|
||||
context = getBaseContext();
|
||||
topAppBar = findViewById(R.id.custom_command_toolbar);
|
||||
topAppBar.setNavigationOnClickListener(view -> onBackPressed());
|
||||
output = findViewById(R.id.custom_command_output);
|
||||
output.setMovementMethod(new ScrollingMovementMethod());
|
||||
|
||||
input = findViewById(R.id.command_edittext);
|
||||
input.requestFocus();
|
||||
|
||||
fab = findViewById(R.id.command_fab);
|
||||
fab.setOnClickListener(view -> {
|
||||
if (isStoragePermissionGranted()){
|
||||
startDownloadService(input.getText().toString(), NotificationUtil.COMMAND_DOWNLOAD_NOTIFICATION_ID);
|
||||
}
|
||||
});
|
||||
|
||||
cancelFab = findViewById(R.id.cancel_command_fab);
|
||||
cancelFab.setOnClickListener(view -> {
|
||||
cancelDownloadService();
|
||||
swapFabs();
|
||||
input.setEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
private void swapFabs(){
|
||||
int cancel = cancelFab.getVisibility();
|
||||
int start = fab.getVisibility();
|
||||
cancelFab.setVisibility(start);
|
||||
fab.setVisibility(cancel);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
|
||||
public void startDownloadService(String command, int id){
|
||||
if(isDownloadServiceRunning) return;
|
||||
Intent serviceIntent = new Intent(context, DownloaderService.class);
|
||||
serviceIntent.putExtra("command", command);
|
||||
serviceIntent.putExtra("id", id);
|
||||
context.getApplicationContext().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
public void stopDownloadService(){
|
||||
Log.e(TAG, String.valueOf(isDownloadServiceRunning));
|
||||
if(!isDownloadServiceRunning) return;
|
||||
iDownloaderService.removeActivity(this);
|
||||
context.getApplicationContext().unbindService(serviceConnection);
|
||||
downloaderService.stopForeground(true);
|
||||
downloaderService.stopSelf();
|
||||
isDownloadServiceRunning = false;
|
||||
}
|
||||
|
||||
public void cancelDownloadService(){
|
||||
if(!isDownloadServiceRunning) return;
|
||||
iDownloaderService.cancelDownload();
|
||||
stopDownloadService();
|
||||
}
|
||||
|
||||
public boolean isStoragePermissionGranted() {
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
}else{
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,30 +1,13 @@
|
||||
package com.deniscerri.ytdl.page;
|
||||
package com.deniscerri.ytdlnis.page;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.deniscerri.ytdl.BuildConfig;
|
||||
import com.deniscerri.ytdl.R;
|
||||
import com.deniscerri.ytdl.util.UpdateUtil;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.yausername.youtubedl_android.YoutubeDL;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
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;
|
||||
import com.deniscerri.ytdlnis.R;
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil;
|
||||
|
||||
public class MoreFragment extends PreferenceFragmentCompat {
|
||||
Preference updateYTDL;
|
||||
@ -1,19 +1,13 @@
|
||||
package com.deniscerri.ytdl.page.settings;
|
||||
package com.deniscerri.ytdlnis.page.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
|
||||
import com.deniscerri.ytdl.R;
|
||||
import com.deniscerri.ytdlnis.R;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
package com.deniscerri.ytdlnis.service;
|
||||
|
||||
import com.deniscerri.ytdlnis.database.Video;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class DownloadInfo {
|
||||
private Video video;
|
||||
private int progress;
|
||||
private LinkedList<Video> downloadQueue;
|
||||
private String outputLine;
|
||||
private String downloadStatus;
|
||||
private String downloadPath;
|
||||
|
||||
public DownloadInfo(){}
|
||||
|
||||
public Video getVideo() {
|
||||
return video;
|
||||
}
|
||||
|
||||
public void setVideo(Video video) {
|
||||
this.video = video;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public LinkedList<Video> getDownloadQueue() {
|
||||
return downloadQueue;
|
||||
}
|
||||
|
||||
public void setDownloadQueue(LinkedList<Video> downloadQueue) {
|
||||
this.downloadQueue = downloadQueue;
|
||||
this.video = downloadQueue.peek();
|
||||
}
|
||||
|
||||
public String getOutputLine() {
|
||||
return outputLine;
|
||||
}
|
||||
|
||||
public void setOutputLine(String outputLine) {
|
||||
this.outputLine = outputLine;
|
||||
}
|
||||
|
||||
public String getDownloadStatus(){
|
||||
return downloadStatus;
|
||||
}
|
||||
|
||||
public void setDownloadStatus(String status){
|
||||
downloadStatus = status;
|
||||
}
|
||||
|
||||
public String getDownloadPath(){
|
||||
return downloadPath;
|
||||
}
|
||||
|
||||
public void setDownloadPath(String path){
|
||||
this.downloadPath = path;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.deniscerri.ytdlnis.service;
|
||||
|
||||
public interface IDownloaderListener {
|
||||
void onDownloadStart(DownloadInfo downloadInfo);
|
||||
void onDownloadProgress(DownloadInfo downloadInfo);
|
||||
void onDownloadError(DownloadInfo downloadInfo);
|
||||
void onDownloadEnd(DownloadInfo downloadInfo);
|
||||
void onDownloadServiceEnd();
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.deniscerri.ytdlnis.service;
|
||||
|
||||
import android.app.Activity;
|
||||
import com.deniscerri.ytdlnis.database.Video;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public interface IDownloaderService {
|
||||
DownloadInfo getDownloadInfo();
|
||||
void addActivity(Activity activity, IDownloaderListener callback);
|
||||
void removeActivity(Activity activity);
|
||||
void updateQueue(ArrayList<Video> queue);
|
||||
void cancelDownload();
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:alpha="1.0"/>
|
||||
</shape>
|
||||
@ -1,4 +1,4 @@
|
||||
package com.deniscerri.ytdl;
|
||||
package com.deniscerri.ytdlnis;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
Download audio and video files on different formats through yt-dlp.
|
||||
Download videos, playlists, multi-select items.
|
||||
Really user friendly.
|
||||
Download History.
|
||||
Command Line Functionality.
|
||||
Material You Compatible.
|
||||
Foreground Download Service.
|
||||
Share links directly to the app.
|
||||
Built in search functionality.
|
||||
Automatically checks for app updates.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
@ -0,0 +1 @@
|
||||
Android YouTube Downloader app
|
||||
@ -0,0 +1 @@
|
||||
YTDLnis
|
||||
Loading…
Reference in New Issue