From 6473f452a1ccdd44c59e53ec264d2d20cf1a9d50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Denis=20=C3=87erri?=
<64997243+deniscerri@users.noreply.github.com>
Date: Fri, 23 Sep 2022 23:55:02 +0200
Subject: [PATCH] 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 readme
---
.idea/deploymentTargetDropDown.xml | 17 +
.idea/misc.xml | 1 +
README.md | 69 ++-
.../ExampleInstrumentedTest.java | 2 +-
app/src/main/AndroidManifest.xml | 2 +-
.../deniscerri/ytdl/DownloaderService.java | 47 ---
.../ytdl/page/CustomCommandActivity.java | 206 ---------
.../com/deniscerri/{ytdl => ytdlnis}/App.java | 13 +-
.../deniscerri/ytdlnis/DownloaderService.java | 374 ++++++++++++++++
.../{ytdl => ytdlnis}/MainActivity.java | 93 +++-
.../adapter/HistoryRecyclerViewAdapter.java | 10 +-
.../adapter/HomeRecyclerViewAdapter.java | 31 +-
.../api/YoutubeAPIManager.java | 63 ++-
.../{ytdl => ytdlnis}/database/DBManager.java | 30 +-
.../{ytdl => ytdlnis}/database/Video.java | 23 +-
.../ytdlnis/page/CustomCommandActivity.java | 175 ++++++++
.../page/HistoryFragment.java | 16 +-
.../{ytdl => ytdlnis}/page/HomeFragment.java | 398 +++++++-----------
.../{ytdl => ytdlnis}/page/MoreFragment.java | 23 +-
.../page/settings/SettingsActivity.java | 10 +-
.../page/settings/SettingsFragment.java | 21 +-
.../ytdlnis/service/DownloadInfo.java | 66 +++
.../ytdlnis/service/IDownloaderListener.java | 9 +
.../ytdlnis/service/IDownloaderService.java | 13 +
.../util/NotificationUtil.java | 13 +-
.../{ytdl => ytdlnis}/util/UpdateUtil.java | 14 +-
.../res/drawable/transparent_background.xml | 4 +
.../res/layout/activity_custom_command.xml | 2 +-
app/src/main/res/layout/activity_main.xml | 2 +-
app/src/main/res/layout/activity_settings.xml | 2 +-
app/src/main/res/layout/result_card.xml | 28 +-
app/src/main/res/values-sq-rAL/strings.xml | 2 +-
app/src/main/res/values/strings.xml | 2 +-
app/src/main/res/xml/root_preferences.xml | 2 +-
.../{ytdl => ytdlnis}/ExampleUnitTest.java | 2 +-
.../android/en-US/full_description.txt | 10 +
.../android/en-US/images/screenshots/1.png | Bin 0 -> 2411568 bytes
.../android/en-US/images/screenshots/2.png | Bin 0 -> 1996889 bytes
.../android/en-US/images/screenshots/3.png | Bin 0 -> 1170014 bytes
.../android/en-US/images/screenshots/4.png | Bin 0 -> 1925057 bytes
.../android/en-US/images/screenshots/5.png | Bin 0 -> 160100 bytes
.../android/en-US/images/screenshots/6.png | Bin 0 -> 2642174 bytes
.../android/en-US/short_description.txt | 1 +
fastlane/metadata/android/en-US/title.txt | 1 +
44 files changed, 1127 insertions(+), 670 deletions(-)
create mode 100644 .idea/deploymentTargetDropDown.xml
rename app/src/androidTest/java/com/deniscerri/{ytdl => ytdlnis}/ExampleInstrumentedTest.java (95%)
delete mode 100644 app/src/main/java/com/deniscerri/ytdl/DownloaderService.java
delete mode 100644 app/src/main/java/com/deniscerri/ytdl/page/CustomCommandActivity.java
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/App.java (90%)
create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/DownloaderService.java
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/MainActivity.java (59%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/adapter/HistoryRecyclerViewAdapter.java (93%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/adapter/HomeRecyclerViewAdapter.java (89%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/api/YoutubeAPIManager.java (88%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/database/DBManager.java (88%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/database/Video.java (80%)
create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/page/CustomCommandActivity.java
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/page/HistoryFragment.java (96%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/page/HomeFragment.java (70%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/page/MoreFragment.java (59%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/page/settings/SettingsActivity.java (79%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/page/settings/SettingsFragment.java (94%)
create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/service/DownloadInfo.java
create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/service/IDownloaderListener.java
create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/service/IDownloaderService.java
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/util/NotificationUtil.java (89%)
rename app/src/main/java/com/deniscerri/{ytdl => ytdlnis}/util/UpdateUtil.java (95%)
create mode 100644 app/src/main/res/drawable/transparent_background.xml
rename app/src/test/java/com/deniscerri/{ytdl => ytdlnis}/ExampleUnitTest.java (91%)
create mode 100644 fastlane/metadata/android/en-US/full_description.txt
create mode 100644 fastlane/metadata/android/en-US/images/screenshots/1.png
create mode 100644 fastlane/metadata/android/en-US/images/screenshots/2.png
create mode 100644 fastlane/metadata/android/en-US/images/screenshots/3.png
create mode 100644 fastlane/metadata/android/en-US/images/screenshots/4.png
create mode 100644 fastlane/metadata/android/en-US/images/screenshots/5.png
create mode 100644 fastlane/metadata/android/en-US/images/screenshots/6.png
create mode 100644 fastlane/metadata/android/en-US/short_description.txt
create mode 100644 fastlane/metadata/android/en-US/title.txt
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
-|||
-|:-------------------------:|:-------------------------:|
-||
-||
+[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