Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed broken YouTube playlist downloader in Drifty GUI #632

Merged
185 changes: 66 additions & 119 deletions CLI/src/main/java/backend/FileDownloader.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,51 @@

import cli.utils.Utility;
import init.Environment;
import properties.LinkType;
import properties.Program;
import support.DownloadMetrics;
import support.Job;
import utils.MessageBroker;

import java.io.*;
import java.net.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static cli.support.Constants.*;
import static properties.Program.YT_DLP;
import static utils.Utility.*;
import static utils.Utility.sleep;

public class FileDownloader implements Runnable {
private static final MessageBroker M = Environment.getMessageBroker();
private final Job job;
private final DownloadMetrics downloadMetrics;
private final int numberOfThreads;
private final long threadMaxDataSize;
private final String dir;
private final boolean isSpotifySong;
private String fileName;
private final String link;
private final Path directoryPath;
private final LinkType linkType;
private String fileName;
private URL url;

public FileDownloader(String link, String fileName, String dir, boolean isSpotifySong) {
link = link.replace('\\', '/');
if (!(link.startsWith("http://") || link.startsWith("https://"))) {
link = "https://" + link;
}
if (link.startsWith("https://github.com/") || (link.startsWith("http://github.com/"))) {
if (!link.endsWith("?raw=true")) {
link = link + "?raw=true";
}
}
this.isSpotifySong = isSpotifySong;
this.link = link;
this.fileName = fileName;
this.dir = dir;
public FileDownloader(Job job) {
this.job = job;
this.link = job.getDownloadLink();
this.linkType = LinkType.getLinkType(link);
this.fileName = job.getFilename();
this.dir = job.getDir();
this.directoryPath = Paths.get(dir);
SaptarshiSarkar12 marked this conversation as resolved.
Show resolved Hide resolved
this.downloadMetrics = new DownloadMetrics();
this.numberOfThreads = downloadMetrics.getThreadCount();
this.threadMaxDataSize = downloadMetrics.getMultiThreadingThreshold();
downloadMetrics.setMultithreading(false);
}

public String getDir() {
if (dir.endsWith(File.separator)) {
return dir;
} else {
return dir + File.separator;
}
}

private void downloadFile() {
try {
ReadableByteChannel readableByteChannel;
Expand All @@ -77,7 +65,6 @@ private void downloadFile() {
File file;
for (int i = 0; i < numberOfThreads; i++) {
file = Files.createTempFile(fileName.hashCode() + String.valueOf(i), ".tmp").toFile();
file.deleteOnExit(); // Deletes temporary file when JVM exits
fileOut = new FileOutputStream(file);
start = i == 0 ? 0 : ((i * partSize) + 1); // The start of the range of bytes to be downloaded by the thread
end = (numberOfThreads - 1) == i ? totalSize : ((i * partSize) + partSize); // The end of the range of bytes to be downloaded by the thread
Expand All @@ -88,31 +75,34 @@ private void downloadFile() {
downloaderThreads.add(downloader);
tempFiles.add(file);
}
ProgressBarThread progressBarThread = new ProgressBarThread(fileOutputStreams, partSizes, fileName, getDir(), totalSize, downloadMetrics);
ProgressBarThread progressBarThread = new ProgressBarThread(fileOutputStreams, partSizes, fileName, dir, totalSize, downloadMetrics);
SaptarshiSarkar12 marked this conversation as resolved.
Show resolved Hide resolved
progressBarThread.start();
M.msgDownloadInfo(String.format(DOWNLOADING_F, fileName));
// check if all the files are downloaded
while (!mergeDownloadedFileParts(fileOutputStreams, partSizes, downloaderThreads, tempFiles)) {
sleep(500);
}
for (File tempFile : tempFiles) {
Files.deleteIfExists(tempFile.toPath());
}
} else {
InputStream urlStream = url.openStream();
readableByteChannel = Channels.newChannel(urlStream);
FileOutputStream fos = new FileOutputStream(getDir() + fileName);
ProgressBarThread progressBarThread = new ProgressBarThread(fos, totalSize, fileName, getDir(), downloadMetrics);
FileOutputStream fos = new FileOutputStream(directoryPath.resolve(fileName).toFile());
ProgressBarThread progressBarThread = new ProgressBarThread(fos, totalSize, fileName, dir, downloadMetrics);
progressBarThread.start();
M.msgDownloadInfo(String.format(DOWNLOADING_F, fileName));
fos.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
fos.close();
urlStream.close();
}
downloadMetrics.setActive(false);
// keep the main thread from closing the IO for a short amount of time so UI thread can finish and give output
// keep the main thread from closing the IO for a short amount of time so the UI thread can finish and give output
Utility.sleep(1800);
} catch (SecurityException e) {
M.msgDownloadError("Write access to \"" + dir + fileName + "\" denied !");
M.msgDownloadError("Write access to the download directory is DENIED! " + e.getMessage());
} catch (FileNotFoundException fileNotFoundException) {
M.msgDownloadError(FILE_NOT_FOUND);
M.msgDownloadError(FILE_NOT_FOUND_ERROR);
} catch (IOException e) {
M.msgDownloadError(FAILED_TO_DOWNLOAD_CONTENTS + e.getMessage());
}
Expand All @@ -121,41 +111,47 @@ private void downloadFile() {
}
}

public void downloadFromYouTube() {
String outputFileName = Objects.requireNonNullElse(fileName, DEFAULT_FILENAME);
String fileDownloadMessage;
if (outputFileName.equals(DEFAULT_FILENAME)) {
fileDownloadMessage = "the YouTube Video";
} else {
fileDownloadMessage = outputFileName;
}
M.msgDownloadInfo("Trying to download \"" + fileDownloadMessage + "\" ...");
ProcessBuilder processBuilder = new ProcessBuilder(Program.get(YT_DLP), "--quiet", "--progress", "-P", dir, link, "-o", outputFileName, "-f", (isSpotifySong ? "bestaudio" : "mp4"));
private void downloadYoutubeOrInstagram(boolean isSpotifySong) {
String[] fullCommand = new String[]{Program.get(Program.YT_DLP), "--quiet", "--progress", "-P", dir, link, "-o", fileName, "-f", (isSpotifySong ? "bestaudio" : "mp4")};
ProcessBuilder processBuilder = new ProcessBuilder(fullCommand);
processBuilder.inheritIO();
M.msgDownloadInfo(String.format(DOWNLOADING_F, fileDownloadMessage));
int exitValueOfYtDlp = -1;
M.msgDownloadInfo(String.format(DOWNLOADING_F, fileName));
Process process;
int exitValueOfYtDlp = 1;
try {
Process ytDlp = processBuilder.start();
ytDlp.waitFor();
exitValueOfYtDlp = ytDlp.exitValue();
process = processBuilder.start();
process.waitFor();
exitValueOfYtDlp = process.exitValue();
} catch (IOException e) {
M.msgDownloadError("An I/O error occurred while initialising YouTube video downloader! " + e.getMessage());
} catch (InterruptedException e) {
M.msgDownloadError("The YouTube video download process was interrupted by user! " + e.getMessage());
M.msgDownloadError("Failed to start download process for \"" + fileName + "\"");
} catch (Exception e) {
String msg = e.getMessage();
String[] messageArray = msg.split(",");
if (messageArray.length >= 1 && messageArray[0].toLowerCase().trim().replaceAll(" ", "").contains("cannotrunprogram")) { // If yt-dlp program is not marked as executable
M.msgDownloadError(DRIFTY_COMPONENT_NOT_EXECUTABLE_ERROR);
} else if (messageArray.length >= 1 && "permissiondenied".equals(messageArray[1].toLowerCase().trim().replaceAll(" ", ""))) { // If a private YouTube / Instagram video is asked to be downloaded
M.msgDownloadError(PERMISSION_DENIED_ERROR);
} else if ("videounavailable".equals(messageArray[0].toLowerCase().trim().replaceAll(" ", ""))) { // If YouTube / Instagram video is unavailable
M.msgDownloadError(VIDEO_UNAVAILABLE_ERROR);
} else {
M.msgDownloadError("An Unknown Error occurred! " + e.getMessage());
}
}
if (exitValueOfYtDlp == 0) {
M.msgDownloadInfo(String.format(SUCCESSFULLY_DOWNLOADED_F, fileDownloadMessage));
M.msgDownloadInfo(String.format(SUCCESSFULLY_DOWNLOADED_F, fileName));
if (isSpotifySong) {
M.msgDownloadInfo("Converting to mp3...");
String conversionProcessMessage = convertToMp3(Paths.get(dir, fileName).toAbsolutePath());
M.msgDownloadInfo("Converting to mp3 ...");
String conversionProcessMessage = utils.Utility.convertToMp3(directoryPath.resolve(fileName).toAbsolutePath());
if (conversionProcessMessage.contains("Failed")) {
M.msgDownloadError(conversionProcessMessage);
} else {
M.msgDownloadInfo(conversionProcessMessage);
M.msgDownloadInfo("Successfully converted to mp3!");
}
}
} else if (exitValueOfYtDlp == 1) {
M.msgDownloadError(String.format(FAILED_TO_DOWNLOAD_F, fileDownloadMessage));
M.msgDownloadError(String.format(FAILED_TO_DOWNLOAD_F, fileName));
} else {
M.msgDownloadError("An Unknown Error occurred! Exit code: " + exitValueOfYtDlp);
}
}

Expand All @@ -179,56 +175,29 @@ public boolean mergeDownloadedFileParts(List<FileOutputStream> fileOutputStreams
}
// check if it is merged-able
if (completed == numberOfThreads) {
fileOutputStream = new FileOutputStream(getDir() + fileName);
long position = 0;
for (int i = 0; i < numberOfThreads; i++) {
File f = tempFiles.get(i);
FileInputStream fs = new FileInputStream(f);
ReadableByteChannel rbs = Channels.newChannel(fs);
fileOutputStream.getChannel().transferFrom(rbs, position, f.length());
position += f.length();
fs.close();
rbs.close();
try (FileOutputStream fos = new FileOutputStream(directoryPath.resolve(fileName).toFile())) {
long position = 0;
for (int i = 0; i < numberOfThreads; i++) {
File f = tempFiles.get(i);
FileInputStream fs = new FileInputStream(f);
ReadableByteChannel rbs = Channels.newChannel(fs);
fos.getChannel().transferFrom(rbs, position, f.length());
position += f.length();
fs.close();
rbs.close();
}
}
fileOutputStream.close();
return true;
}
return false;
}

@Override
public void run() {
boolean isYouTubeLink = isYoutube(link);
boolean isInstagramLink = isInstagram(link);
try {
// If the link is of a YouTube or Instagram video, then the following block of code will execute.
if (isYouTubeLink || isInstagramLink) {
try {
if (isYouTubeLink) {
downloadFromYouTube();
} else {
downloadFromInstagram();
}
} catch (InterruptedException e) {
M.msgDownloadError(USER_INTERRUPTION);
} catch (Exception e) {
if (isYouTubeLink) {
M.msgDownloadError(YOUTUBE_DOWNLOAD_FAILED);
} else {
M.msgDownloadError(INSTAGRAM_DOWNLOAD_FAILED);
}
String msg = e.getMessage();
String[] messageArray = msg.split(",");
if (messageArray.length >= 1 && messageArray[0].toLowerCase().trim().replaceAll(" ", "").contains("cannotrunprogram")) { // If yt-dlp program is not marked as executable
M.msgDownloadError(DRIFTY_COMPONENT_NOT_EXECUTABLE);
} else if (messageArray.length >= 1 && "permissiondenied".equals(messageArray[1].toLowerCase().trim().replaceAll(" ", ""))) { // If a private YouTube / Instagram video is asked to be downloaded
M.msgDownloadError(PERMISSION_DENIED);
} else if ("videounavailable".equals(messageArray[0].toLowerCase().trim().replaceAll(" ", ""))) { // If YouTube / Instagram video is unavailable
M.msgDownloadError(VIDEO_UNAVAILABLE);
} else {
M.msgDownloadError("An Unknown Error occurred! " + e.getMessage());
}
}
if (linkType.equals(LinkType.YOUTUBE) || linkType.equals(LinkType.INSTAGRAM)) {
downloadYoutubeOrInstagram(LinkType.getLinkType(job.getSourceLink()).equals(LinkType.SPOTIFY));
} else {
url = new URI(link).toURL();
URLConnection openConnection = url.openConnection();
Expand All @@ -250,26 +219,4 @@ public void run() {
M.msgDownloadError(String.format(FAILED_CONNECTION_F, url));
}
}

private void downloadFromInstagram() throws InterruptedException, IOException {
String outputFileName = Objects.requireNonNullElse(fileName, DEFAULT_FILENAME);
String fileDownloadMessage;
if (outputFileName.equals(DEFAULT_FILENAME)) {
fileDownloadMessage = "the Instagram Video";
} else {
fileDownloadMessage = outputFileName;
}
M.msgDownloadInfo("Trying to download \"" + fileDownloadMessage + "\" ...");
ProcessBuilder processBuilder = new ProcessBuilder(Program.get(YT_DLP), "--quiet", "--progress", "-P", dir, link, "-o", outputFileName); // The command line arguments tell `yt-dlp` to download the video and to save it to the specified directory.
processBuilder.inheritIO();
M.msgDownloadInfo(String.format(DOWNLOADING_F, fileDownloadMessage));
Process instagramDownloadProcess = processBuilder.start(); // Starts the download process
instagramDownloadProcess.waitFor();
int exitStatus = instagramDownloadProcess.exitValue();
if (exitStatus == 0) {
M.msgDownloadInfo(String.format(SUCCESSFULLY_DOWNLOADED_F, fileDownloadMessage));
} else if (exitStatus == 1) {
M.msgDownloadError(String.format(FAILED_TO_DOWNLOAD_F, fileDownloadMessage));
}
}
}
12 changes: 7 additions & 5 deletions CLI/src/main/java/backend/ProgressBarThread.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -150,14 +151,15 @@ private String generateProgressBar() {

private void cleanup() {
downloadMetrics.setProgressPercent(0f);
String path = Paths.get(dir).resolve(fileName).toAbsolutePath().toString();
if (isMultiThreadedDownloading) {
String sizeWithUnit = UnitConverter.format(totalDownloadedBytes, 2);
System.out.println();
M.msgDownloadInfo(SUCCESSFULLY_DOWNLOADED + fileName + OF_SIZE + sizeWithUnit + " at " + dir + fileName);
System.out.print("\r");
M.msgDownloadInfo(String.format(SUCCESSFULLY_DOWNLOADED_F, fileName) + OF_SIZE + sizeWithUnit + " at \"" + path + "\"");
} else if (downloadedBytes == totalDownloadedBytes) {
String sizeWithUnit = UnitConverter.format(downloadedBytes, 2);
System.out.println();
M.msgDownloadInfo(SUCCESSFULLY_DOWNLOADED + fileName + OF_SIZE + sizeWithUnit + " at " + dir + fileName);
System.out.print("\r");
M.msgDownloadInfo(String.format(SUCCESSFULLY_DOWNLOADED_F, fileName) + OF_SIZE + sizeWithUnit + " at \"" + path + "\"");
} else {
System.out.println();
M.msgDownloadError(DOWNLOAD_FAILED);
Expand Down Expand Up @@ -209,7 +211,7 @@ public void run() {
}
}
} catch (IOException e) {
M.msgDownloadError("Error while downloading " + fileName + " : " + e.getMessage());
M.msgDownloadError("Error while downloading \"" + fileName + "\" : " + e.getMessage());
} finally {
downloading = downloadMetrics.isActive();
}
Expand Down
5 changes: 0 additions & 5 deletions CLI/src/main/java/cli/support/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ public class Constants extends support.Constants {
public static final String BANNER_BORDER = "====================================================================";
public static final String FAILED_TO_DOWNLOAD_CONTENTS = "Failed to download the contents ! ";
public static final String FAILED_READING_STREAM = "Failed to get I/O operations channel to read from the data stream !";
public static final String DEFAULT_FILENAME = "%(title)s.%(ext)s";
public static final String SUCCESSFULLY_DOWNLOADED = "Successfully downloaded ";
public static final String OF_SIZE = " of size ";
public static final String DOWNLOAD_FAILED = "Download failed!";
public static final String USER_INTERRUPTION = "User interrupted while downloading the YouTube/Instagram Video!";
public static final String YOUTUBE_DOWNLOAD_FAILED = "Failed to download YouTube video!";
public static final String INSTAGRAM_DOWNLOAD_FAILED = "Failed to download Instagram video!";
public static final String ENTER_Y_OR_N = "Please enter Y for yes and N for no!";
}
Loading
Loading