diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..d53f09fd --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 917ec63c..cef79b0f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -64,6 +64,7 @@ + diff --git a/README.md b/README.md index f85ed78e..4cec4e66 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,56 @@ -# ytdlnis -Android App for downloading youtube videos -Created as a project for the class, Mobile Programming. +

+ ytldnis +

+

+ Android App for downloading videos using yt-dlp +

+

+ Created as a project for the class, Mobile Programming. Continued to update it because why not. +

-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 -|![image](https://user-images.githubusercontent.com/64997243/185808455-0b04f573-afe5-43d8-b6c8-6481b799e474.png)|![image](https://user-images.githubusercontent.com/64997243/185808467-7fcc9938-f124-45c1-8b29-3384269fb693.png)| -|:-------------------------:|:-------------------------:| -![image](https://user-images.githubusercontent.com/64997243/185808474-5df57756-6e27-4c03-9870-3c47f782bfb9.png)|![image](https://user-images.githubusercontent.com/64997243/185808477-073a8ea8-70ae-4702-9865-317149f2bb25.png)| -![image](https://user-images.githubusercontent.com/64997243/185808483-9b5e62c3-f28b-4bfb-9caf-adbb899ce702.png)|![image](https://user-images.githubusercontent.com/64997243/185808495-9930312c-34df-43f1-b7f3-e519d1231c09.png)| +[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 + +
+ + + + + + +
+ + +## 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. \ No newline at end of file diff --git a/app/src/androidTest/java/com/deniscerri/ytdl/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/deniscerri/ytdlnis/ExampleInstrumentedTest.java similarity index 95% rename from app/src/androidTest/java/com/deniscerri/ytdl/ExampleInstrumentedTest.java rename to app/src/androidTest/java/com/deniscerri/ytdlnis/ExampleInstrumentedTest.java index d5997f2e..bf95e48d 100644 --- a/app/src/androidTest/java/com/deniscerri/ytdl/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/com/deniscerri/ytdlnis/ExampleInstrumentedTest.java @@ -1,4 +1,4 @@ -package com.deniscerri.ytdl; +package com.deniscerri.ytdlnis; import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53d5818d..3c06d882 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com.deniscerri.ytdlnis" > diff --git a/app/src/main/java/com/deniscerri/ytdl/DownloaderService.java b/app/src/main/java/com/deniscerri/ytdl/DownloaderService.java deleted file mode 100644 index 6600590c..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/DownloaderService.java +++ /dev/null @@ -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; - } - } -} diff --git a/app/src/main/java/com/deniscerri/ytdl/page/CustomCommandActivity.java b/app/src/main/java/com/deniscerri/ytdl/page/CustomCommandActivity.java deleted file mode 100644 index 8c2f074e..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/page/CustomCommandActivity.java +++ /dev/null @@ -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; - } - } -} diff --git a/app/src/main/java/com/deniscerri/ytdl/App.java b/app/src/main/java/com/deniscerri/ytdlnis/App.java similarity index 90% rename from app/src/main/java/com/deniscerri/ytdl/App.java rename to app/src/main/java/com/deniscerri/ytdlnis/App.java index 4ecccabb..16a862c0 100644 --- a/app/src/main/java/com/deniscerri/ytdl/App.java +++ b/app/src/main/java/com/deniscerri/ytdlnis/App.java @@ -1,24 +1,18 @@ -package com.deniscerri.ytdl; +package com.deniscerri.ytdlnis; import android.app.Application; -import android.os.Environment; -import android.provider.ContactsContract; import android.util.Log; import android.widget.Toast; import androidx.preference.PreferenceManager; -import com.deniscerri.ytdl.util.NotificationUtil; -import com.deniscerri.ytdl.util.UpdateUtil; +import com.deniscerri.ytdlnis.util.NotificationUtil; import com.google.android.material.color.DynamicColors; +import com.yausername.aria2c.Aria2c; import com.yausername.ffmpeg.FFmpeg; import com.yausername.youtubedl_android.YoutubeDL; import com.yausername.youtubedl_android.YoutubeDLException; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; - import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.exceptions.UndeliverableException; @@ -75,6 +69,7 @@ public class App extends Application { private void initLibraries() throws YoutubeDLException { YoutubeDL.getInstance().init(this); FFmpeg.getInstance().init(this); + Aria2c.getInstance().init(this); } private void createNotificationChannels() { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.java b/app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.java new file mode 100644 index 00000000..0d584aff --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.java @@ -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 activities = new ConcurrentHashMap<>(); + private DownloadInfo downloadInfo = new DownloadInfo(); + private LinkedList