Skip to content

Commit

Permalink
Fixed broken YouTube playlist downloader in Drifty GUI (#632)
Browse files Browse the repository at this point in the history
* fix(GUI): Fixed broken youtube playlist downloader

* fix(GUI): Fixed broken youtube playlist downloader support by centralizing file download data and removing redundant calls to fetch download data

* style: format codebase

* chore: Fixed linter issues

* fix: Fixed CoderabbitAI's reviews and github-advanced-security bot's warnings

* chore: Fixed linter issues

* style: format codebase

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
SaptarshiSarkar12 and github-actions[bot] authored Sep 20, 2024
1 parent 7b460f2 commit aac9a81
Show file tree
Hide file tree
Showing 34 changed files with 953 additions and 1,045 deletions.
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);
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);
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

0 comments on commit aac9a81

Please sign in to comment.