diff --git a/CLI/src/main/java/backend/FileDownloader.java b/CLI/src/main/java/backend/FileDownloader.java index b14edadcd..b7dc82c89 100644 --- a/CLI/src/main/java/backend/FileDownloader.java +++ b/CLI/src/main/java/backend/FileDownloader.java @@ -2,8 +2,10 @@ 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.*; @@ -11,54 +13,40 @@ 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; @@ -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 @@ -88,18 +75,21 @@ 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); @@ -107,12 +97,12 @@ private void downloadFile() { 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()); } @@ -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); } } @@ -179,18 +175,18 @@ public boolean mergeDownloadedFileParts(List 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; @@ -198,37 +194,10 @@ public boolean mergeDownloadedFileParts(List fileOutputStreams @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(); @@ -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)); - } - } } diff --git a/CLI/src/main/java/backend/ProgressBarThread.java b/CLI/src/main/java/backend/ProgressBarThread.java index 45985d667..0ed84e95d 100644 --- a/CLI/src/main/java/backend/ProgressBarThread.java +++ b/CLI/src/main/java/backend/ProgressBarThread.java @@ -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; @@ -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); @@ -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(); } diff --git a/CLI/src/main/java/cli/support/Constants.java b/CLI/src/main/java/cli/support/Constants.java index cef32bd13..bfb03b90a 100644 --- a/CLI/src/main/java/cli/support/Constants.java +++ b/CLI/src/main/java/cli/support/Constants.java @@ -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!"; } diff --git a/CLI/src/main/java/cli/utils/Utility.java b/CLI/src/main/java/cli/utils/Utility.java index 331c3546f..c2cd4d50a 100644 --- a/CLI/src/main/java/cli/utils/Utility.java +++ b/CLI/src/main/java/cli/utils/Utility.java @@ -1,62 +1,26 @@ package cli.utils; import cli.init.Environment; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; -import java.util.*; +import java.util.Scanner; -import static cli.support.Constants.*; +import static cli.support.Constants.ENTER_Y_OR_N; public class Utility extends utils.Utility { private static final Scanner SC = ScannerFactory.getInstance(); - public static String findFilenameInLink(String link) { - String filename = ""; - if (isInstagram(link) || isYoutube(link)) { - LinkedList linkMetadataList = Utility.getYtDlpMetadata(link); - for (String json : Objects.requireNonNull(linkMetadataList)) { - filename = Utility.getFilenameFromJson(json); - } - if (filename.isEmpty()) { - msgBroker.msgFilenameError("Filename detection failed: No filename found in metadata!"); - return null; - } - } else { - // Example: "example.com/file.txt" prints "Filename detected: file.txt" - // example.com/file.json -> file.json - String file = link.substring(link.lastIndexOf("/") + 1); - if (file.isEmpty()) { - msgBroker.msgFilenameError(FILENAME_DETECTION_ERROR); - return null; - } - int index = file.lastIndexOf("."); - if (index < 0) { - msgBroker.msgFilenameError(FILENAME_DETECTION_ERROR); - return null; - } - String extension = file.substring(index); - // edge case 1: "example.com/." - if (extension.length() == 1) { - msgBroker.msgFilenameError(FILENAME_DETECTION_ERROR); - return null; - } - // file.png?width=200 -> file.png - filename = file.split("([?])")[0]; - msgBroker.msgFilenameInfo(FILENAME_DETECTED + "\"" + filename + "\""); - } - return filename; - } - - public boolean yesNoValidation(String input, String printMessage) { + public boolean yesNoValidation(String input, String printMessage, boolean isWarning) { while (input.isEmpty()) { Environment.getMessageBroker().msgInputError(ENTER_Y_OR_N, true); msgBroker.msgLogError(ENTER_Y_OR_N); - Environment.getMessageBroker().msgInputInfo(printMessage, false); + if (isWarning) { + Environment.getMessageBroker().msgHistoryWarning(printMessage, false); + } else { + Environment.getMessageBroker().msgInputInfo(printMessage, false); + } input = SC.nextLine().toLowerCase(); } char choice = input.charAt(0); @@ -67,49 +31,14 @@ public boolean yesNoValidation(String input, String printMessage) { } else { Environment.getMessageBroker().msgInputError("Invalid input!", true); msgBroker.msgLogError("Invalid input!"); - Environment.getMessageBroker().msgInputInfo(printMessage, false); - input = SC.nextLine().toLowerCase(); - return yesNoValidation(input, printMessage); - } - } - - public static String getSpotifyDownloadLink(String spotifyMetadataJson) { - JsonObject jsonObject = JsonParser.parseString(spotifyMetadataJson).getAsJsonObject(); - String songName = jsonObject.get("songName").getAsString(); - int duration = jsonObject.get("duration").getAsInt(); - JsonArray artists = jsonObject.get("artists").getAsJsonArray(); - ArrayList artistNames = new ArrayList<>(artists.size()); - for (int i = 0; i < artists.size(); i++) { - artistNames.add(artists.get(i).getAsString()); - } - String query = (String.join(", ", artistNames) + " - " + songName).toLowerCase(); - ArrayList> searchResults = getYoutubeSearchResults(query, true); - boolean searchedWithFilters = true; - if (searchResults == null) { - msgBroker.msgLogError("Failed to get search results for the song with filters! Trying without filters ..."); - searchResults = getYoutubeSearchResults(query, false); - searchedWithFilters = false; - if (searchResults == null) { - msgBroker.msgDownloadError("Song is exclusive to Spotify and cannot be downloaded!"); - return null; - } - } - String matchedId = getMatchingVideoID(Objects.requireNonNull(searchResults), duration, artistNames); - if (matchedId.isEmpty()) { - if (searchedWithFilters) { - msgBroker.msgLogError("Failed to get a matching video ID for the song with filters! Trying without filters ..."); - searchResults = getYoutubeSearchResults(query, false); - matchedId = getMatchingVideoID(Objects.requireNonNull(searchResults), duration, artistNames); - if (matchedId.isEmpty()) { - msgBroker.msgDownloadError("Song is exclusive to Spotify and cannot be downloaded!"); - return null; - } + if (isWarning) { + Environment.getMessageBroker().msgHistoryWarning(printMessage, false); } else { - msgBroker.msgDownloadError("Song is exclusive to Spotify and cannot be downloaded!"); - return null; + Environment.getMessageBroker().msgInputInfo(printMessage, false); } + input = SC.nextLine().toLowerCase(); + return yesNoValidation(input, printMessage, isWarning); } - return "https://www.youtube.com/watch?v=" + matchedId; } public static Yaml getYamlParser() { diff --git a/CLI/src/main/java/main/Drifty_CLI.java b/CLI/src/main/java/main/Drifty_CLI.java index c5609029e..75ed0e359 100644 --- a/CLI/src/main/java/main/Drifty_CLI.java +++ b/CLI/src/main/java/main/Drifty_CLI.java @@ -6,15 +6,14 @@ import cli.utils.MessageBroker; import cli.utils.ScannerFactory; import cli.utils.Utility; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.apache.commons.io.FileUtils; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.error.YAMLException; import preferences.AppSettings; +import properties.LinkType; import properties.MessageType; import properties.OS; import properties.Program; +import support.DownloadConfiguration; import support.Job; import support.JobHistory; import updater.UpdateChecker; @@ -23,20 +22,22 @@ import java.io.*; import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.Charset; import java.nio.file.*; import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import static cli.support.Constants.*; -import static cli.utils.Utility.*; +import static cli.utils.Utility.isURL; +import static cli.utils.Utility.sleep; public class Drifty_CLI { public static final Logger LOGGER = Logger.getInstance(); protected static final Scanner SC = ScannerFactory.getInstance(); protected static JobHistory jobHistory; - protected static boolean isYoutubeURL; - protected static boolean isInstagramLink; - protected static boolean isSpotifyLink; + private static LinkType linkType; private static MessageBroker messageBroker; private static String link; private static String downloadsFolder; @@ -62,8 +63,6 @@ public static void main(String[] args) { printBanner(); if (args.length > 0) { link = null; - String name = null; - String location = null; for (int i = 0; i < args.length; i++) { switch (args[i]) { case HELP_FLAG, HELP_FLAG_SHORT -> { @@ -72,7 +71,7 @@ public static void main(String[] args) { } case NAME_FLAG, NAME_FLAG_SHORT -> { if (i + 1 < args.length) { - name = args[i + 1]; + fileName = args[i + 1]; i++; // Skip the next iteration as we have already processed this argument } else { messageBroker.msgInitError("No filename specified!"); @@ -81,7 +80,7 @@ public static void main(String[] args) { } case LOCATION_FLAG, LOCATION_FLAG_SHORT -> { if (i + 1 < args.length) { - location = args[i + 1]; + downloadsFolder = args[i + 1]; i++; // Skip the next iteration as we have already processed this argument } else { messageBroker.msgInitError("No download directory specified!"); @@ -143,7 +142,7 @@ public static void main(String[] args) { if ("all".equalsIgnoreCase(args[i + 1])) { messageBroker.msgInputInfo(REMOVE_ALL_URL_CONFIRMATION, false); String choiceString = SC.nextLine().toLowerCase(); - boolean choice = utility.yesNoValidation(choiceString, REMOVE_ALL_URL_CONFIRMATION); + boolean choice = utility.yesNoValidation(choiceString, REMOVE_ALL_URL_CONFIRMATION, false); if (choice) { removeAllUrls(); } @@ -183,6 +182,10 @@ public static void main(String[] args) { } } if (!batchDownloading) { + if (link == null) { + messageBroker.msgInitError("No URL specified! Exiting..."); + Environment.terminate(1); + } boolean isUrlValid; if (Utility.isURL(link)) { isUrlValid = Utility.isLinkValid(link); @@ -191,39 +194,12 @@ public static void main(String[] args) { messageBroker.msgLinkError(INVALID_LINK); } if (isUrlValid) { - isYoutubeURL = isYoutube(link); - isInstagramLink = isInstagram(link); - isSpotifyLink = isSpotify(link); - downloadsFolder = location; + linkType = LinkType.getLinkType(link); downloadsFolder = getProperDownloadsFolder(downloadsFolder); - if ((name == null) && (fileName == null || fileName.isEmpty())) { - if (isSpotifyLink && link.contains("playlist")) { - handleSpotifyPlaylist(); - } else { - if (isInstagram(link)) { - link = formatInstagramLink(link); - } - messageBroker.msgFilenameInfo("Retrieving filename from link..."); - HashMap spotifyMetadata; - if (isSpotifyLink) { - spotifyMetadata = Utility.getSpotifySongMetadata(link); - if (spotifyMetadata != null && !spotifyMetadata.isEmpty()) { - fileName = spotifyMetadata.get("name").toString() + ".webm"; - messageBroker.msgFilenameInfo(FILENAME_DETECTED + "\"" + fileName + "\""); - } else { - messageBroker.msgFilenameError(FILENAME_DETECTION_ERROR); - } - } else { - fileName = findFilenameInLink(link); - } - if (fileName != null && !fileName.isEmpty()) { - verifyJobAndDownload(false); - } - } + if (linkType.equals(LinkType.SPOTIFY) && link.contains("playlist")) { + handleSpotifyPlaylist(); } else { - fileName = name; - messageBroker.msgFilenameInfo("Filename provided : " + fileName); - verifyJobAndDownload(false); + verifyJobAndDownload(); } } } @@ -235,12 +211,11 @@ public static void main(String[] args) { messageBroker.msgInputInfo("Select download option :", true); messageBroker.msgInputInfo("\t1. Batch Download (Download Multiple files)", true); messageBroker.msgInputInfo("\t2. Single File Download (Download One file at a time)", true); - int choice = SC.nextInt(); - if (choice == 1) { + String choice = SC.nextLine().strip(); + if ("1".equals(choice)) { batchDownloading = true; messageBroker.msgInputInfo("Enter the path to the YAML data file : ", false); - batchDownloadingFile = SC.next(); - SC.nextLine(); + batchDownloadingFile = SC.nextLine().split(" ")[0]; if (!(batchDownloadingFile.endsWith(".yml") || batchDownloadingFile.endsWith(".yaml"))) { messageBroker.msgBatchError("The data file should be a YAML file!"); } else { @@ -250,7 +225,7 @@ public static void main(String[] args) { batchDownloader(); break; } - } else if (choice == 2) { + } else if ("2".equals(choice)) { batchDownloading = false; break; } else { @@ -259,8 +234,7 @@ public static void main(String[] args) { } if (!batchDownloading) { messageBroker.msgInputInfo(ENTER_FILE_LINK, false); - link = SC.next().strip(); - SC.nextLine(); + link = SC.nextLine().strip(); messageBroker.msgInputInfo("Validating link...", true); if (Utility.isURL(link)) { Utility.isLinkValid(link); @@ -268,47 +242,23 @@ public static void main(String[] args) { messageBroker.msgLinkError(INVALID_LINK); continue; } + linkType = LinkType.getLinkType(link); messageBroker.msgInputInfo("Download directory (\".\" for default or \"L\" for " + AppSettings.GET.lastDownloadFolder() + ") : ", false); - downloadsFolder = SC.next().strip(); + downloadsFolder = SC.nextLine().split(" ")[0]; downloadsFolder = getProperDownloadsFolder(downloadsFolder); - isYoutubeURL = isYoutube(link); - isInstagramLink = isInstagram(link); - isSpotifyLink = isSpotify(link); - if (isSpotifyLink) { - if (link.contains("playlist")) { - handleSpotifyPlaylist(); - } else { - messageBroker.msgFilenameInfo("Retrieving filename from link..."); - HashMap spotifyMetadata = Utility.getSpotifySongMetadata(link); - if (spotifyMetadata != null && !spotifyMetadata.isEmpty()) { - fileName = spotifyMetadata.get("songName").toString() + ".webm"; - messageBroker.msgFilenameInfo(FILENAME_DETECTED + "\"" + fileName + "\""); - } else { - messageBroker.msgFilenameError(FILENAME_DETECTION_ERROR); - } - if (fileName != null && !fileName.isEmpty()) { - verifyJobAndDownload(true); - } - } + if (linkType.equals(LinkType.SPOTIFY) && link.contains("playlist")) { + handleSpotifyPlaylist(); } else { - if (isInstagram(link)) { - link = formatInstagramLink(link); - } - messageBroker.msgFilenameInfo("Retrieving filename from link..."); - fileName = findFilenameInLink(link); - if (fileName != null && !fileName.isEmpty()) { - verifyJobAndDownload(true); - } else { - messageBroker.msgFilenameError("Failed to retrieve filename from link!"); - } + verifyJobAndDownload(); } } messageBroker.msgInputInfo(QUIT_OR_CONTINUE, true); - String choice = SC.next().toLowerCase().strip(); + String choice = SC.nextLine().split(" ")[0].toLowerCase(); if ("q".equals(choice)) { LOGGER.log(MessageType.INFO, CLI_APPLICATION_TERMINATED); break; } + fileName = null; printBanner(); } Environment.terminate(0); @@ -354,7 +304,7 @@ private static void handleUpdateAvailable(boolean askForInstallingUpdate) { private static boolean getUserConfirmation() { messageBroker.msgUpdateInfo("Do you want to download the update? (Enter Y for yes and N for no) : "); String choiceString = SC.nextLine().toLowerCase(); - return utility.yesNoValidation(choiceString, "Do you want to download the update? (Enter Y for yes and N for no) : "); + return utility.yesNoValidation(choiceString, "Do you want to download the update? (Enter Y for yes and N for no) : ", false); } private static void downloadAndUpdate() { @@ -394,7 +344,7 @@ private static boolean downloadUpdate() { File tmpFolder = Files.createTempDirectory("Drifty").toFile(); tmpFolder.deleteOnExit(); File latestExecutableFile = Paths.get(tmpFolder.getPath()).resolve(currentExecutableFile.getName()).toFile(); - FileDownloader downloader = new FileDownloader(updateURL.toString(), currentExecutableFile.getName(), tmpFolder.toString(), false); + FileDownloader downloader = new FileDownloader(new Job(updateURL.toString(), tmpFolder.toString(), currentExecutableFile.getName(), updateURL.toString())); downloader.run(); if (latestExecutableFile.exists() && latestExecutableFile.isFile() && latestExecutableFile.length() > 0) { // If the latest executable was successfully downloaded, set the executable permission and execute the update. @@ -432,21 +382,26 @@ private static void printVersion() { private static void handleSpotifyPlaylist() { messageBroker.msgFilenameInfo("Retrieving the number of tracks in the playlist..."); - ArrayList> playlistMetadata = Utility.getSpotifyPlaylistMetadata(link); - if (!batchDownloading) { - SC.nextLine(); // To remove 'whitespace' from input buffer. The whitespace will not be present in the input buffer if the user is using batch downloading because only yml file is parsed but no user input is taken. + DownloadConfiguration config = new DownloadConfiguration(link, downloadsFolder, null); + try (ExecutorService executor = Executors.newSingleThreadExecutor()) { + Future future = executor.submit(config::fetchFileData); + future.get(); + } catch (ExecutionException e) { + messageBroker.msgLinkError("Failed to retrieve spotify playlist metadata! " + e.getMessage()); + } catch (InterruptedException e) { + messageBroker.msgLinkError("User interrupted the process of retrieving spotify playlist metadata! " + e.getMessage()); } - if (playlistMetadata != null && !playlistMetadata.isEmpty()) { - for (HashMap songMetadata : playlistMetadata) { + ArrayList> playlistData = config.getFileData(); + if (playlistData != null && !playlistData.isEmpty()) { + int numberOfTracks = playlistData.size(); + for (HashMap songData : playlistData) { messageBroker.msgStyleInfo(BANNER_BORDER); - messageBroker.msgLinkInfo("[" + (playlistMetadata.indexOf(songMetadata) + 1) + "/" + playlistMetadata.size() + "] " + "Processing link : " + songMetadata.get("link")); - link = songMetadata.get("link").toString(); - fileName = cleanFilename(songMetadata.get("songName").toString()) + ".webm"; + link = songData.get("link").toString(); + messageBroker.msgLinkInfo("[" + (playlistData.indexOf(songData) + 1) + "/" + numberOfTracks + "] " + "Processing link : " + link); + fileName = songData.get("filename").toString(); + String downloadLink = songData.get("downloadLink").toString(); messageBroker.msgFilenameInfo(FILENAME_DETECTED + "\"" + fileName + "\""); - songMetadata.remove("link"); - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - String songMetadataJson = gson.toJson(songMetadata); - checkHistoryAndDownload(new Job(link, downloadsFolder, fileName, songMetadataJson, false), false); + checkHistoryAndDownload(new Job(link, downloadsFolder, fileName, downloadLink)); } } else { messageBroker.msgLinkError("Failed to retrieve playlist metadata!"); @@ -515,9 +470,7 @@ private static void batchDownloader() { messageBroker.msgLinkError("Invalid URL : " + link); continue; } - isYoutubeURL = isYoutube(link); - isInstagramLink = isInstagram(link); - isSpotifyLink = isSpotify(link); + linkType = LinkType.getLinkType(link); if (".".equals(downloadsFolder)) { downloadsFolder = Utility.getHomeDownloadFolder(); } else if ("L".equalsIgnoreCase(downloadsFolder)) { @@ -531,32 +484,11 @@ private static void batchDownloader() { } if (data.containsKey("fileNames") && !data.get("fileNames").get(i).isEmpty()) { fileName = data.get("fileNames").get(i); - } else { - if (isSpotifyLink && link.contains("playlist")) { - fileName = null; - } else { - if (isInstagram(link)) { - link = formatInstagramLink(link); - } - messageBroker.msgFilenameInfo("Retrieving filename from link..."); - if (isSpotifyLink) { - HashMap spotifyMetadata = Utility.getSpotifySongMetadata(link); - if (spotifyMetadata != null && !spotifyMetadata.isEmpty()) { - fileName = spotifyMetadata.get("songName").toString() + ".webm"; - messageBroker.msgFilenameInfo(FILENAME_DETECTED + "\"" + fileName + "\""); - } else { - fileName = cleanFilename("Unknown_Filename_") + randomString(15) + ".webm"; - messageBroker.msgFilenameError(FILENAME_DETECTION_ERROR); - } - } else { - fileName = findFilenameInLink(link); - } - } } - if (isSpotifyLink && link.contains("playlist")) { + if (linkType.equals(LinkType.SPOTIFY) && link.contains("playlist")) { handleSpotifyPlaylist(); - } else if (fileName != null && !fileName.isEmpty()) { - verifyJobAndDownload(false); + } else { + verifyJobAndDownload(); } } } catch (IOException e) { @@ -573,23 +505,22 @@ private static void batchDownloader() { } } - private static void renameFilenameIfRequired(boolean removeInputBufferFirst) { // Asks the user if the detected filename is to be used or not. If not, then the user is asked to enter a filename. - if ((fileName == null || (fileName.isEmpty())) && (!isYoutubeURL && !isInstagramLink && !isSpotifyLink)) { + private static void renameFilenameIfRequired() { // Asks the user if the detected filename is to be used or not. If not, then the user is asked to enter a filename. + if ((fileName == null || (fileName.isEmpty())) && linkType.equals(LinkType.OTHER)) { messageBroker.msgInputInfo(ENTER_FILE_NAME_WITH_EXTENSION, false); - if (removeInputBufferFirst) { - SC.nextLine(); - } fileName = SC.nextLine(); } else { messageBroker.msgInputInfo("Would you like to use this filename? (Enter Y for yes and N for no) : ", false); - if (removeInputBufferFirst) { - SC.nextLine(); // To remove 'whitespace' from input buffer. The whitespace will not be present in the input buffer if the user is using batch downloading because only yml file is parsed but no user input is taken. - } String choiceString = SC.nextLine().toLowerCase(); - boolean choice = utility.yesNoValidation(choiceString, "Would you like to use this filename? (Enter Y for yes and N for no) : "); + boolean choice = utility.yesNoValidation(choiceString, "Would you like to use this filename? (Enter Y for yes and N for no) : ", false); if (!choice) { messageBroker.msgInputInfo(ENTER_FILE_NAME_WITH_EXTENSION, false); - fileName = SC.nextLine(); + String tempFileName = SC.nextLine(); + if (tempFileName.isEmpty()) { + messageBroker.msgFilenameError("No filename specified! Using the detected filename."); + } else { + fileName = tempFileName; + } } } } @@ -657,29 +588,44 @@ public static void printBanner() { System.out.println(ANSI_PURPLE + BANNER_BORDER + ANSI_RESET); } - private static void verifyJobAndDownload(boolean removeInputBufferFirst) { - Job job; - if (isSpotifyLink) { - File spotifyMetadataFile = Program.getJsonDataPath().resolve("spotify-metadata.json").toFile(); - if (spotifyMetadataFile.exists()) { - try { - String json = FileUtils.readFileToString(spotifyMetadataFile, Charset.defaultCharset()); - job = new Job(link, downloadsFolder, fileName, json, false); - } catch (IOException e) { - messageBroker.msgFilenameError("Failed to read Spotify metadata file! " + e.getMessage()); - return; + private static void verifyJobAndDownload() { + DownloadConfiguration config = new DownloadConfiguration(link, downloadsFolder, fileName); + config.sanitizeLink(); + messageBroker.msgFilenameInfo("Retrieving file data..."); + try (ExecutorService executor = Executors.newSingleThreadExecutor()) { + Future future = executor.submit(config::fetchFileData); + future.get(); + } catch (ExecutionException e) { + messageBroker.msgLinkError("Failed to retrieve file metadata! " + e.getMessage()); + } catch (InterruptedException e) { + messageBroker.msgLinkError("User interrupted the process of retrieving file metadata! " + e.getMessage()); + } + if (config.getStatusCode() != 0) { + messageBroker.msgLinkError("Failed to fetch file data!"); + return; + } + ArrayList> fileData = config.getFileData(); + if (fileData != null && !fileData.isEmpty()) { + for (HashMap data : fileData) { + link = data.get("link").toString(); + fileName = data.get("filename").toString(); + String downloadLink = null; + if (data.containsKey("downloadLink")) { + downloadLink = data.get("downloadLink").toString(); } - } else { - messageBroker.msgFilenameError("Spotify metadata file not found!"); - return; + if (fileData.size() > 1) { + messageBroker.msgStyleInfo(BANNER_BORDER); + messageBroker.msgLinkInfo("[" + (fileData.indexOf(data) + 1) + "/" + fileData.size() + "] " + "Processing link : " + link); + } + messageBroker.msgFilenameInfo(FILENAME_DETECTED + "\"" + fileName + "\""); + checkHistoryAndDownload(new Job(link, downloadsFolder, fileName, downloadLink)); } } else { - job = new Job(link, downloadsFolder, fileName, false); + checkHistoryAndDownload(new Job(link, downloadsFolder, fileName, null)); } - checkHistoryAndDownload(job, removeInputBufferFirst); } - private static void checkHistoryAndDownload(Job job, boolean removeInputBufferFirst) { + private static void checkHistoryAndDownload(Job job) { boolean doesFileExist = job.fileExists(); boolean hasHistory = jobHistory.exists(link); boolean fileExistsHasHistory = doesFileExist && hasHistory; @@ -687,47 +633,37 @@ private static void checkHistoryAndDownload(Job job, boolean removeInputBufferFi if (fileExistsNoHistory) { fileName = Utility.renameFile(fileName, downloadsFolder); messageBroker.msgHistoryWarning(String.format(MSG_FILE_EXISTS_NO_HISTORY + "\n", job.getFilename(), job.getDir(), fileName), false); - renameFilenameIfRequired(true); - if (isSpotifyLink) { - messageBroker.msgDownloadInfo("Trying to get download link for \"" + link + "\""); - link = Utility.getSpotifyDownloadLink(job.getSpotifyMetadataJson()); - } + renameFilenameIfRequired(); if (link != null) { - job = new Job(link, downloadsFolder, fileName, false); + job = new Job(link, downloadsFolder, fileName, null); jobHistory.addJob(job, true); - FileDownloader downloader = new FileDownloader(link, fileName, downloadsFolder, isSpotifyLink); + FileDownloader downloader = new FileDownloader(job); downloader.run(); } } else if (fileExistsHasHistory) { messageBroker.msgHistoryWarning(String.format(MSG_FILE_EXISTS_HAS_HISTORY, job.getFilename(), job.getDir()), false); - if (removeInputBufferFirst) { - SC.nextLine(); - } String choiceString = SC.nextLine().toLowerCase(); - boolean choice = utility.yesNoValidation(choiceString, String.format(MSG_FILE_EXISTS_HAS_HISTORY, job.getFilename(), job.getDir())); + boolean choice = utility.yesNoValidation(choiceString, String.format(MSG_FILE_EXISTS_HAS_HISTORY, job.getFilename(), job.getDir()), true); if (choice) { fileName = Utility.renameFile(fileName, downloadsFolder); messageBroker.msgFilenameInfo("New file name : " + fileName); - renameFilenameIfRequired(false); - if (isSpotifyLink) { - link = Utility.getSpotifyDownloadLink(link); - } + renameFilenameIfRequired(); if (link != null) { - job = new Job(link, downloadsFolder, fileName, false); + job = new Job(link, downloadsFolder, fileName, null); jobHistory.addJob(job, true); - FileDownloader downloader = new FileDownloader(link, fileName, downloadsFolder, isSpotifyLink); + FileDownloader downloader = new FileDownloader(job); downloader.run(); } + } else { + messageBroker.msgHistoryWarning("Download cancelled!", false); + System.out.println(); } } else { jobHistory.addJob(job, true); - renameFilenameIfRequired(removeInputBufferFirst); - if (isSpotifyLink) { - messageBroker.msgDownloadInfo("Trying to get download link for \"" + link + "\""); - link = Utility.getSpotifyDownloadLink(job.getSpotifyMetadataJson()); - } + renameFilenameIfRequired(); if (link != null) { - FileDownloader downloader = new FileDownloader(link, fileName, downloadsFolder, isSpotifyLink); + job = new Job(link, downloadsFolder, fileName, job.getDownloadLink()); + FileDownloader downloader = new FileDownloader(job); downloader.run(); } } @@ -766,7 +702,7 @@ private static String normalizeUrl(String urlString) { } private static void ensureYamlFileExists() { - // Check if the links queue file exists, and create it if it doesn't + // Check if the links queue file exists and create it if it doesn't File yamlFile = new File(yamlFilePath); messageBroker.msgLogInfo("Checking if links queue file (" + yamlFilePath + ") exists..."); if (!yamlFile.exists()) { diff --git a/CLI/src/main/resources/META-INF/native-image/reflect-config.json b/CLI/src/main/resources/META-INF/native-image/reflect-config.json index cbcb4727b..50b8ea042 100644 --- a/CLI/src/main/resources/META-INF/native-image/reflect-config.json +++ b/CLI/src/main/resources/META-INF/native-image/reflect-config.json @@ -64,6 +64,9 @@ "name":"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"java.beans.Introspector" +}, { "name":"java.lang.Class", "methods":[{"name":"getRecordComponents","parameterTypes":[] }, {"name":"isRecord","parameterTypes":[] }] @@ -103,6 +106,9 @@ { "name":"java.sql.Date" }, +{ + "name":"java.sql.Timestamp" +}, { "name":"java.util.Date" }, diff --git a/CLI/src/main/resources/META-INF/native-image/resource-config.json b/CLI/src/main/resources/META-INF/native-image/resource-config.json index c60492f05..b24032db0 100644 --- a/CLI/src/main/resources/META-INF/native-image/resource-config.json +++ b/CLI/src/main/resources/META-INF/native-image/resource-config.json @@ -18,10 +18,6 @@ "pattern":"\\QMETA-INF/services/javax.xml.parsers.DocumentBuilderFactory\\E" }, { "pattern":"\\QMETA-INF/services/javax.xml.transform.TransformerFactory\\E" - }, { - "pattern":"\\Qffmpeg\\E" - }, { - "pattern":"\\Qyt-dlp\\E" }, { "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfkc.nrm\\E" }, { diff --git a/Core/src/main/java/init/Environment.java b/Core/src/main/java/init/Environment.java index 7708c040e..36fc84d3b 100644 --- a/Core/src/main/java/init/Environment.java +++ b/Core/src/main/java/init/Environment.java @@ -21,7 +21,7 @@ import static properties.Program.YT_DLP; public class Environment { - private static MessageBroker msgBroker = Environment.getMessageBroker(); + private static MessageBroker msgBroker; private static boolean isAdministrator; /* @@ -31,9 +31,10 @@ public class Environment { Finally, it updates yt-dlp if it has not been updated in the last 24 hours. */ public static void initializeEnvironment() { + msgBroker = Environment.getMessageBroker(); msgBroker.msgLogInfo("OS : " + OS.getOSName()); - Utility.initializeUtility(); // Lazy initialization of the MessageBroker in Utility class isAdministrator = hasAdminPrivileges(); + Utility.initializeUtility(); // Lazy initialization of the MessageBroker in Utility class new Thread(() -> AppSettings.SET.driftyUpdateAvailable(UpdateChecker.isUpdateAvailable())).start(); ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); executor.scheduleAtFixedRate(Utility.setSpotifyAccessToken(), 0, 3480, java.util.concurrent.TimeUnit.SECONDS); // Thread to refresh Spotify access token every 58 minutes @@ -117,27 +118,19 @@ public static boolean isYtDLPUpdated() { public static boolean hasAdminPrivileges() { try { - msgBroker.msgLogInfo("Determining current executable folder path..."); Path currentExecutableFolderPath = Paths.get(Utility.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); - msgBroker.msgLogInfo("Current executable folder path: " + currentExecutableFolderPath); - Path adminTestFilePath = currentExecutableFolderPath.resolve("adminTestFile.txt"); - msgBroker.msgLogInfo("Creating test file at: " + adminTestFilePath); Files.createFile(adminTestFilePath); - - msgBroker.msgLogInfo("Deleting test file at: " + adminTestFilePath); Files.deleteIfExists(adminTestFilePath); - - msgBroker.msgLogInfo("Admin privileges confirmed."); return true; } catch (URISyntaxException e) { - msgBroker.msgInitError("Failed to get the current executable path! " + e.getMessage()); + System.out.println("Failed to get the current executable path! " + e.getMessage()); return false; } catch (AccessDeniedException e) { - msgBroker.msgInitError("You are not running Drifty as an administrator! " + e.getMessage()); + System.out.println("You are not running Drifty as an administrator! " + e.getMessage()); return false; } catch (IOException e) { - msgBroker.msgInitError("Failed to create a file in the current executable folder! " + e.getMessage()); + System.out.println("Failed to create a file in the current executable folder! " + e.getMessage()); return false; } } diff --git a/Core/src/main/java/preferences/Clear.java b/Core/src/main/java/preferences/Clear.java index 51d107df2..d4cef47b6 100644 --- a/Core/src/main/java/preferences/Clear.java +++ b/Core/src/main/java/preferences/Clear.java @@ -58,4 +58,8 @@ public void latestDriftyVersionTag() { public void driftyUpdateAvailable() { preferences.remove(DRIFTY_UPDATE_AVAILABLE); } + + public void jobs() { + preferences.remove(JOBS); + } } diff --git a/Core/src/main/java/preferences/Get.java b/Core/src/main/java/preferences/Get.java index 77c2f5b04..7c2f21a38 100644 --- a/Core/src/main/java/preferences/Get.java +++ b/Core/src/main/java/preferences/Get.java @@ -7,9 +7,11 @@ import org.hildan.fxgson.FxGson; import properties.Program; import support.JobHistory; +import support.Jobs; import utils.Utility; import javax.crypto.*; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; @@ -20,6 +22,7 @@ import java.util.prefs.Preferences; import static preferences.Labels.*; +import static properties.Program.JOB_FILE; import static properties.Program.JOB_HISTORY_FILE; public class Get { @@ -121,4 +124,21 @@ public String latestDriftyVersionTag() { public boolean driftyUpdateAvailable() { return preferences.getBoolean(DRIFTY_UPDATE_AVAILABLE, false); } + + public Jobs jobs() { + GsonBuilder gsonBuilder = new GsonBuilder(); + Gson gson = FxGson.addFxSupport(gsonBuilder).setPrettyPrinting().create(); + Path jobBatchFile = Paths.get(Program.get(JOB_FILE)); + try { + String json = FileUtils.readFileToString(jobBatchFile.toFile(), Charset.defaultCharset()); + if (json != null && !json.isEmpty()) { + return gson.fromJson(json, Jobs.class); + } + } catch (FileNotFoundException e) { + Environment.getMessageBroker().msgLogInfo("Job file not found: " + jobBatchFile); + } catch (IOException e) { + Environment.getMessageBroker().msgLogError("Failed to read job file: " + jobBatchFile); + } + return new Jobs(); + } } diff --git a/Core/src/main/java/preferences/Labels.java b/Core/src/main/java/preferences/Labels.java index 23809afc0..6f501462b 100644 --- a/Core/src/main/java/preferences/Labels.java +++ b/Core/src/main/java/preferences/Labels.java @@ -16,4 +16,5 @@ public interface Labels { String LAST_DRIFTY_UPDATE_TIME = "LAST_DRIFTY_UPDATE_TIME"; String EARLY_ACCESS = "EARLY_ACCESS"; String DRIFTY_UPDATE_AVAILABLE = "DRIFTY_UPDATE_AVAILABLE"; + String JOBS = "JOBS"; } diff --git a/Core/src/main/java/preferences/Set.java b/Core/src/main/java/preferences/Set.java index b37e9dc41..71f4e213d 100644 --- a/Core/src/main/java/preferences/Set.java +++ b/Core/src/main/java/preferences/Set.java @@ -7,6 +7,7 @@ import org.hildan.fxgson.FxGson; import properties.Program; import support.JobHistory; +import support.Jobs; import javax.crypto.*; import java.io.IOException; @@ -19,6 +20,7 @@ import java.util.prefs.Preferences; import static preferences.Labels.*; +import static properties.Program.JOB_FILE; import static properties.Program.JOB_HISTORY_FILE; public class Set { @@ -124,4 +126,27 @@ public void driftyUpdateAvailable(boolean isUpdateAvailable) { AppSettings.CLEAR.driftyUpdateAvailable(); preferences.putBoolean(DRIFTY_UPDATE_AVAILABLE, isUpdateAvailable); } + + public void jobs(Jobs jobs) { + String serializedJobs = serializeJobs(jobs); + writeJobsToFile(serializedJobs); + } + + private String serializeJobs(Jobs jobs) { + GsonBuilder gsonBuilder = new GsonBuilder(); + Gson gson = FxGson.addFxSupport(gsonBuilder).setPrettyPrinting().create(); + return gson.toJson(jobs); + } + + private void writeJobsToFile(String serializedJobs) { + AppSettings.CLEAR.jobs(); + Path jobBatchFile = Paths.get(Program.get(JOB_FILE)); + try { + FileUtils.writeStringToFile(jobBatchFile.toFile(), serializedJobs, Charset.defaultCharset()); + } catch (IOException e) { + String errorMessage = "Failed to write jobs to file: " + e.getMessage(); + Environment.getMessageBroker().msgInitError(errorMessage); + throw new RuntimeException(errorMessage, e); + } + } } diff --git a/Core/src/main/java/support/Constants.java b/Core/src/main/java/support/Constants.java index a92cf37c5..75a3fce3d 100644 --- a/Core/src/main/java/support/Constants.java +++ b/Core/src/main/java/support/Constants.java @@ -14,15 +14,15 @@ public class Constants { public static final String INVALID_LINK = "Link is invalid! Please check the link and try again."; public static final String FILENAME_DETECTION_ERROR = "Failed to detect the filename! A default name will be used instead."; public static final String TRYING_TO_AUTO_DETECT_DOWNLOADS_FOLDER = "Trying to automatically detect default Downloads folder..."; - public static final String FAILED_TO_RETRIEVE_DEFAULT_DOWNLOAD_FOLDER = "Failed to retrieve default download folder!"; + public static final String FAILED_TO_RETRIEVE_DEFAULT_DOWNLOAD_FOLDER_ERROR = "Failed to retrieve default download folder!"; public static final String FOLDER_DETECTED = "Default download folder detected : "; public static final String FILENAME_DETECTED = "Filename detected : "; - public static final String FAILED_TO_CREATE_LOG = "Failed to create log : "; - public static final String FAILED_TO_CLEAR_LOG = "Failed to clear Log contents !"; - public static final String FILE_NOT_FOUND = "An error occurred! Requested file does not exist, please check the url."; - public static final String VIDEO_UNAVAILABLE = "The requested video is unavailable, it has been deleted from the platform."; - public static final String PERMISSION_DENIED = "You do not have access to download the video, permission is denied."; - public static final String DRIFTY_COMPONENT_NOT_EXECUTABLE = "A Drifty component (yt-dlp) is not marked as executable."; + public static final String FAILED_TO_CREATE_LOG_ERROR = "Failed to create log : "; + public static final String FAILED_TO_CLEAR_LOG_ERROR = "Failed to clear Log contents !"; + public static final String FILE_NOT_FOUND_ERROR = "An error occurred! Requested file does not exist, please check the url."; + public static final String VIDEO_UNAVAILABLE_ERROR = "The requested video is unavailable, it has been deleted from the platform."; + public static final String PERMISSION_DENIED_ERROR = "You do not have access to download the video, permission is denied."; + public static final String DRIFTY_COMPONENT_NOT_EXECUTABLE_ERROR = "A Drifty component (yt-dlp) is not marked as executable."; public static final String USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"; public static final long ONE_DAY = 86400000; // Value of one day (24 Hours) in milliseconds public static URL updateURL; @@ -43,6 +43,6 @@ public class Constants { */ public static final String DOWNLOADING_F = "Downloading \"%s\" ..."; public static final String FAILED_CONNECTION_F = "Failed to connect to %s!"; - public static final String SUCCESSFULLY_DOWNLOADED_F = "Successfully downloaded %s!"; + public static final String SUCCESSFULLY_DOWNLOADED_F = "Successfully downloaded \"%s\""; public static final String FAILED_TO_DOWNLOAD_F = "Failed to download %s!"; } diff --git a/Core/src/main/java/support/DownloadConfiguration.java b/Core/src/main/java/support/DownloadConfiguration.java new file mode 100644 index 000000000..0e2c658db --- /dev/null +++ b/Core/src/main/java/support/DownloadConfiguration.java @@ -0,0 +1,278 @@ +package support; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import init.Environment; +import preferences.AppSettings; +import properties.LinkType; +import properties.Mode; +import utils.MessageBroker; +import utils.Utility; + +import java.util.*; +import java.util.concurrent.*; + +import static utils.Utility.cleanFilename; +import static utils.Utility.randomString; + +public class DownloadConfiguration { + private final String directory; + private final ArrayList> fileData; + private final LinkType linkType; + private final String filename; + private final MessageBroker msgBroker = Environment.getMessageBroker(); + private String link; + private int fileCount; + private int filesProcessed; + private int statusCode; + + public DownloadConfiguration(String link, String directory, String filename) { + this.link = link; + this.directory = directory; + this.filename = filename; + this.linkType = LinkType.getLinkType(link); + this.fileData = new ArrayList<>(); + } + + public void sanitizeLink() { + link = link.trim(); + link = link.replace('\\', '/'); + link = link.replaceFirst("^(?!https?:)", "https://"); + if (link.startsWith("https://github.com/") || (link.startsWith("http://github.com/"))) { + if (!link.endsWith("?raw=true")) { + link = link + "?raw=true"; + } + } + if (this.linkType.equals(LinkType.INSTAGRAM)) { + this.link = Utility.formatInstagramLink(link); + } + } + + public int fetchFileData() { + return switch (this.linkType) { + case YOUTUBE -> processYouTubeLink(); + case SPOTIFY -> processSpotifyLink(); + case INSTAGRAM -> processInstagramLink(); + default -> processFileLink(); + }; + } + + private int processYouTubeLink() { + String jsonString = Utility.getYtDlpMetadata(link); + if (jsonString == null || jsonString.isEmpty()) { + msgBroker.msgLogError("Failed to process Youtube Link"); + statusCode = -1; + return -1; + } + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + if (link.contains("playlist")) { + JsonArray entries = jsonObject.get("entries").getAsJsonArray(); + fileCount = entries.size(); + for (JsonElement entry : entries) { + if (Mode.isCLI()) { + System.out.print("\r[" + (filesProcessed + 1) + "/" + fileCount + "] Processing Youtube Playlist..."); + } + JsonObject entryObject = entry.getAsJsonObject(); + String videoLink = entryObject.get("url").getAsString(); + String videoTitle = cleanFilename(entryObject.get("title").getAsString()); + if (videoTitle.isEmpty()) { + videoTitle = "Unknown_YouTube_Video_".concat(randomString(5)); + } + String detectedFilename = videoTitle.concat(".mp4"); + HashMap data = new HashMap<>(); + data.put("link", videoLink); + data.put("filename", detectedFilename); + data.put("directory", this.directory); + fileData.add(data); + filesProcessed++; + } + if (Mode.isCLI()) { + System.out.println("\rYoutube Playlist processed successfully"); + } else { + msgBroker.msgLinkInfo("Youtube Playlist processed successfully"); + } + } else { + msgBroker.msgLinkInfo("Processing Youtube Video..."); + fileCount = 1; + HashMap data = new HashMap<>(); + String videoTitle = cleanFilename(jsonObject.get("title").getAsString()); + if (videoTitle.isEmpty()) { + videoTitle = "Unknown_YouTube_Video_".concat(randomString(5)); + } + String detectedFilename = videoTitle.concat(".mp4"); + data.put("link", link); + data.put("filename", this.filename == null ? detectedFilename : this.filename); + data.put("directory", this.directory); + fileData.add(data); + filesProcessed++; + msgBroker.msgLinkInfo("Youtube Video processed successfully"); + } + if (fileData.isEmpty()) { + statusCode = -1; + return -1; + } else { + statusCode = 0; + return 0; + } + } + + private int processSpotifyLink() { + if (link.contains("playlist")) { + ArrayList> playlistMetadata = Utility.getSpotifyPlaylistMetadata(link); + if (playlistMetadata != null && !playlistMetadata.isEmpty()) { + fileCount = playlistMetadata.size(); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + if (Mode.isGUI()) { + executor.scheduleAtFixedRate(this::updateJobList, 1500, 600, TimeUnit.MILLISECONDS); + } + for (HashMap songMetadata : playlistMetadata) { + if (Mode.isCLI()) { + System.out.print("\r[" + filesProcessed + "/" + fileCount + "] Processing Spotify Playlist..."); + } else { + msgBroker.msgLinkInfo("[" + filesProcessed + "/" + fileCount + "] Processing Spotify Playlist..."); + } + HashMap data = Utility.getSpotifySongDownloadData(songMetadata, this.directory); + if (data == null) { + msgBroker.msgLogError("Failed to process Spotify Playlist"); + filesProcessed++; + statusCode = -1; + return -1; + } + fileData.add(data); + filesProcessed++; + } + if (Mode.isCLI()) { + System.out.println("\rSpotify Playlist processed successfully"); + } else { + msgBroker.msgLinkInfo("Spotify Playlist processed successfully"); + } + if (Mode.isGUI()) { + executor.shutdown(); + } + } + } else { + HashMap songMetadata = Utility.getSpotifySongMetadata(link); + fileCount = 1; + if (songMetadata != null && !songMetadata.isEmpty()) { + msgBroker.msgLinkInfo("Processing Spotify Song..."); + HashMap data = Utility.getSpotifySongDownloadData(songMetadata, this.directory); + if (data == null) { + msgBroker.msgLogError("Failed to process Spotify Song"); + filesProcessed++; + statusCode = -1; + return -1; + } + if (this.filename != null) { + data.put("filename", this.filename); + } + fileData.add(data); + filesProcessed++; + msgBroker.msgLinkInfo("Spotify Song processed successfully"); + } + } + if (fileData.isEmpty()) { + statusCode = -1; + return -1; + } else { + statusCode = 0; + return 0; + } + } + + private int processInstagramLink() { + String jsonString = Utility.getYtDlpMetadata(link); + fileCount = 1; + if (jsonString != null && !jsonString.isEmpty()) { + msgBroker.msgLinkInfo("Processing Instagram Post..."); + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + HashMap data = new HashMap<>(); + String instagramVideoName = cleanFilename(jsonObject.get("title").getAsString()); + if (instagramVideoName.isEmpty()) { + instagramVideoName = "Unknown_Instagram_Video_".concat(randomString(5)); + } + String detectedFilename = instagramVideoName.concat(".").concat(jsonObject.get("ext").getAsString()); + data.put("link", link); + data.put("filename", this.filename == null ? detectedFilename : this.filename); + data.put("directory", this.directory); + fileData.add(data); + filesProcessed++; + msgBroker.msgLinkInfo("Instagram Post processed successfully"); + } + if (fileData.isEmpty()) { + statusCode = -1; + return -1; + } else { + statusCode = 0; + return 0; + } + } + + private int processFileLink() { + msgBroker.msgLinkInfo("Processing File Link..."); + fileCount = 1; + HashMap data = new HashMap<>(); + String detectedFilename = cleanFilename(Utility.extractFilenameFromURL(link)); + if (detectedFilename.isEmpty()) { + detectedFilename = "Unknown_File_".concat(randomString(5)); + } + data.put("link", link); + data.put("filename", this.filename == null ? detectedFilename : this.filename); + data.put("directory", this.directory); + fileData.add(data); + filesProcessed++; + msgBroker.msgLinkInfo("File Link processed successfully"); + if (fileData.isEmpty()) { + statusCode = -1; + return -1; + } else { + statusCode = 0; + return 0; + } + } + + public void updateJobList() { + Map distinctJobList = new ConcurrentHashMap<>(); + for (Job job : AppSettings.GET.jobs().jobList()) { + distinctJobList.put(job.hashCode(), job); + } + if (fileData.isEmpty()) { + return; + } + for (HashMap data : fileData) { + String link = data.get("link").toString(); + String filename = data.get("filename").toString(); + String directory = data.get("directory").toString(); + Job job; + if (linkType.equals(LinkType.SPOTIFY)) { + String downloadLink = data.get("downloadLink").toString(); + job = new Job(link, directory, filename, downloadLink); + } else { + job = new Job(link, directory, filename, null); + } + distinctJobList.put(job.hashCode(), job); + } + AppSettings.GET.jobs().setList(new ConcurrentLinkedDeque<>(distinctJobList.values())); + } + + public String getLink() { + return link; + } + + public int getFileCount() { + return fileCount; + } + + public int getFilesProcessed() { + return filesProcessed; + } + + public ArrayList> getFileData() { + return fileData; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/Core/src/main/java/support/Job.java b/Core/src/main/java/support/Job.java index 968f20f8c..2fb0483d1 100644 --- a/Core/src/main/java/support/Job.java +++ b/Core/src/main/java/support/Job.java @@ -1,85 +1,78 @@ package support; import java.io.File; -import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; public class Job { private final String link; private final String dir; private final String filename; - private String spotifyMetadataJson; - private boolean repeatDownload; + private final String downloadLink; - public Job(String link, String dir, String filename, boolean repeatDownload) { + public Job(String link, String dir, String filename, String downloadLink) { this.link = link; + this.downloadLink = downloadLink; this.dir = dir; this.filename = filename; - this.repeatDownload = repeatDownload; - } - - public Job(String link, String dir, String filename, String spotifyMetadataJson, boolean repeatDownload) { - this.link = link; - this.dir = dir; - this.filename = filename; - this.repeatDownload = repeatDownload; - this.spotifyMetadataJson = spotifyMetadataJson; - } - - public Job(String link, String dir) { - this.link = link; - this.dir = dir; - this.filename = getName(); - } - - public boolean matches(Job otherJob) { - return otherJob.getLink().equals(link) && otherJob.getDir().equals(dir) && otherJob.getFilename().equals(filename); } public boolean matchesLink(Job job) { - return job.getLink().equals(link); + return job.getSourceLink().equals(link); } public boolean matchesLink(String link) { return this.link.equals(link); } - public String getLink() { + public String getSourceLink() { return link; } + public String getDownloadLink() { + if (downloadLink != null) { + return downloadLink; + } + if (link != null) { + return link; + } + throw new IllegalStateException("Both link and downloadLink are null"); + } + public String getDir() { return dir; } public String getFilename() { - return this.filename; + return filename; } public File getFile() { - return Paths.get(dir, filename).toFile(); + return Paths.get(dir).resolve(filename).toFile(); } public boolean fileExists() { - Path path = Paths.get(dir, filename); - return path.toFile().exists(); + return getFile().exists(); } - private String getName() { - String[] nameParts = link.split("/"); - return nameParts[nameParts.length - 1]; - } - - public String getSpotifyMetadataJson() { - return spotifyMetadataJson; + @Override + public boolean equals(Object obj) { + if (obj instanceof Job job) { + return Objects.equals(job.getSourceLink(), link) && + Objects.equals(job.getDir(), dir) && + Objects.equals(job.getFilename(), filename); + } + return false; } - public boolean repeatOK() { - return repeatDownload; + @Override + public int hashCode() { + return Objects.hash(link, dir, filename); } @Override public String toString() { + // This method returns only the filename, else the hashCodes will appear in the ListView return filename; } } diff --git a/Core/src/main/java/support/JobHistory.java b/Core/src/main/java/support/JobHistory.java index 6ad216c7e..ebe162d77 100644 --- a/Core/src/main/java/support/JobHistory.java +++ b/Core/src/main/java/support/JobHistory.java @@ -34,7 +34,7 @@ public void clear() { public boolean exists(String link) { for (Job job : jobHistoryList) { - if (job.getLink().equals(link)) { + if (job.getSourceLink().equals(link)) { return true; } } diff --git a/GUI/src/main/java/gui/support/Jobs.java b/Core/src/main/java/support/Jobs.java similarity index 91% rename from GUI/src/main/java/gui/support/Jobs.java rename to Core/src/main/java/support/Jobs.java index 5384c5a0b..0fae9294d 100644 --- a/GUI/src/main/java/gui/support/Jobs.java +++ b/Core/src/main/java/support/Jobs.java @@ -1,7 +1,6 @@ -package gui.support; +package support; -import gui.preferences.AppSettings; -import support.Job; +import preferences.AppSettings; import java.util.concurrent.ConcurrentLinkedDeque; @@ -21,7 +20,7 @@ public ConcurrentLinkedDeque jobList() { public void add(Job newJob) { for (Job job : jobList) { - if (job.matches(newJob)) { + if (job.equals(newJob)) { return; } } diff --git a/Core/src/main/java/utils/Logger.java b/Core/src/main/java/utils/Logger.java index cba944236..b0072b51d 100644 --- a/Core/src/main/java/utils/Logger.java +++ b/Core/src/main/java/utils/Logger.java @@ -10,8 +10,8 @@ import java.text.SimpleDateFormat; import java.util.Calendar; -import static support.Constants.FAILED_TO_CLEAR_LOG; -import static support.Constants.FAILED_TO_CREATE_LOG; +import static support.Constants.FAILED_TO_CLEAR_LOG_ERROR; +import static support.Constants.FAILED_TO_CREATE_LOG_ERROR; public final class Logger { boolean isLogEmpty; @@ -33,12 +33,13 @@ private Logger() { } private File determineLogFile() { - if (!Environment.isAdministrator()) { - try { - return Files.createTempFile(logFilename.split("\\.")[0], ".log").toFile(); - } catch (IOException e) { - System.err.println(FAILED_TO_CREATE_LOG + logFilename); - } + if (Environment.hasAdminPrivileges()) { + return new File(logFilename); // Log file will be created in the same directory as the executable + } + try { + return Files.createTempFile(logFilename.split("\\.")[0], ".log").toFile(); // Log file will be created in the temp directory + } catch (IOException e) { + System.err.println(FAILED_TO_CREATE_LOG_ERROR + logFilename); } return new File(logFilename); } @@ -64,7 +65,7 @@ private void clearLog() { isLogEmpty = true; logWriter.write(""); } catch (IOException e) { - System.err.println(FAILED_TO_CLEAR_LOG); + System.err.println(FAILED_TO_CLEAR_LOG_ERROR); } } @@ -77,7 +78,7 @@ public void log(MessageType messageType, String logMessage) { isLogEmpty = true; logWriter.println(dateAndTime + " " + messageType.toString() + " - " + logMessage); } catch (IOException e) { - System.err.println(FAILED_TO_CREATE_LOG + logMessage); + System.err.println(FAILED_TO_CREATE_LOG_ERROR + logMessage); } } } diff --git a/Core/src/main/java/utils/Utility.java b/Core/src/main/java/utils/Utility.java index 78066a689..1b108f4b1 100644 --- a/Core/src/main/java/utils/Utility.java +++ b/Core/src/main/java/utils/Utility.java @@ -5,7 +5,6 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.text.StringEscapeUtils; -import org.hildan.fxgson.FxGson; import preferences.AppSettings; import properties.MessageCategory; import properties.Mode; @@ -38,6 +37,7 @@ public class Utility { private static final Random RANDOM_GENERATOR = new Random(System.currentTimeMillis()); protected static MessageBroker msgBroker; private static boolean interrupted; + private static String ytDlpErrorMessage; public static void initializeUtility() { // Lazy initialization of the MessageBroker as it might be null when the Environment MessageBroker is not set @@ -59,10 +59,6 @@ public static boolean isSpotify(String url) { return url.matches(pattern); } - public static boolean isExtractableLink(String link) { - return isYoutube(link) || isInstagram(link) || isSpotify(link); - } - public static boolean isOffline() { try { URL projectWebsite = URI.create(DRIFTY_WEBSITE_URL).toURL(); @@ -137,9 +133,8 @@ public static URL getUpdateURL() throws MalformedURLException, URISyntaxExceptio return updateURL; } - public static LinkedList getYtDlpMetadata(String link) { + public static String getYtDlpMetadata(String link) { try { - LinkedList list = new LinkedList<>(); File driftyJsonFolder = Program.getJsonDataPath().toFile(); if (driftyJsonFolder.exists() && driftyJsonFolder.isDirectory()) { FileUtils.forceDelete(driftyJsonFolder); // Deletes the previously generated temporary directory for Drifty @@ -164,20 +159,21 @@ public static LinkedList getYtDlpMetadata(String link) { return null; } File[] files = driftyJsonFolder.listFiles(); + String linkMetadata; if (files != null) { for (File file : files) { if ("yt-metadata.info.json".equals(file.getName())) { - String linkMetadata = FileUtils.readFileToString(file, Charset.defaultCharset()); - list.addLast(linkMetadata); + linkMetadata = FileUtils.readFileToString(file, Charset.defaultCharset()); + FileUtils.forceDelete(driftyJsonFolder); // delete the metadata files of Drifty from the config directory + return linkMetadata; } } - FileUtils.forceDelete(driftyJsonFolder); // delete the metadata files of Drifty from the config directory } - return list; } catch (IOException e) { msgBroker.msgLinkError("Failed to perform I/O operations on link metadata! " + e.getMessage()); return null; } + return null; } public static String getHomeDownloadFolder() { @@ -191,21 +187,13 @@ public static String getHomeDownloadFolder() { } if (downloadsFolder.equals(FileSystems.getDefault().getSeparator())) { downloadsFolder = System.getProperty("user.home"); - msgBroker.msgDirError(FAILED_TO_RETRIEVE_DEFAULT_DOWNLOAD_FOLDER); + msgBroker.msgDirError(FAILED_TO_RETRIEVE_DEFAULT_DOWNLOAD_FOLDER_ERROR); } else { msgBroker.msgDirInfo(FOLDER_DETECTED + downloadsFolder); } return downloadsFolder; } - public static String makePretty(String json) { - // The regex strings won't match unless the json string is converted to pretty format - GsonBuilder g = new GsonBuilder(); - Gson gson = FxGson.addFxSupport(g).setPrettyPrinting().create(); - JsonElement element = JsonParser.parseString(json); - return gson.toJson(element); - } - public static String renameFile(String filename, String dir) { Path path = Paths.get(dir, filename); String newFilename = filename; @@ -223,22 +211,6 @@ public static String renameFile(String filename, String dir) { return newFilename; } - public static String getFilenameFromJson(String jsonString) { - String json = makePretty(jsonString); - String filename; - String regexFilename = "(\"title\": \")(.+)(\",)"; - Pattern p = Pattern.compile(regexFilename); - Matcher m = p.matcher(json); - if (m.find()) { - filename = cleanFilename(m.group(2)) + ".mp4"; - msgBroker.msgFilenameInfo(FILENAME_DETECTED + "\"" + filename + "\""); - } else { - filename = cleanFilename("Unknown_Filename_") + randomString(15) + ".mp4"; - msgBroker.msgFilenameError(FILENAME_DETECTION_ERROR); - } - return filename; - } - public static HashMap getSpotifySongMetadata(String songUrl) { Pattern trackPattern = Pattern.compile("/track/(\\w+)"); Matcher trackMatcher = trackPattern.matcher(songUrl); @@ -272,10 +244,18 @@ public static HashMap getSpotifySongMetadata(String songUrl) { String retryAfter = songMetadataResponse.headers().firstValue("retry-after").orElse("5"); long timeToWait = (long) parseStringToInt(retryAfter, "Failed to parse time to wait before retrying!", MessageCategory.DOWNLOAD) * 1000; for (long i = timeToWait; i >= 0; i -= 1000) { - System.out.print("\r" + "Retrying in " + i / 1000 + " seconds..."); + if (Mode.isCLI()) { + System.out.print("\r" + "Retrying in " + i / 1000 + " seconds..."); + } else { + msgBroker.msgLinkInfo("Retrying in " + i / 1000 + " seconds..."); + } sleep(1000); } - System.out.println("\r" + "Retrying now..."); + if (Mode.isCLI()) { + System.out.println("\r" + "Retrying now..."); + } else { + msgBroker.msgLinkInfo("Retrying now..."); + } continue; } else { return null; @@ -290,6 +270,7 @@ public static HashMap getSpotifySongMetadata(String songUrl) { HashMap songMetadataMap = new HashMap<>(); Gson gson = new GsonBuilder().setPrettyPrinting().create(); JsonElement artistJsonTree = gson.toJsonTree(artistNames); + songMetadataMap.put("link", songUrl); songMetadataMap.put("songName", songName); songMetadataMap.put("duration", duration); songMetadataMap.put("artists", artistJsonTree); @@ -391,12 +372,10 @@ public static ArrayList> getSpotifyPlaylistMetadata(Stri artistNames.add(artists.get(j).getAsJsonObject().get("name").getAsString()); } HashMap songMetadataMap = new HashMap<>(); - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - JsonElement artistJsonTree = gson.toJsonTree(artistNames); songMetadataMap.put("link", link); songMetadataMap.put("songName", songName); songMetadataMap.put("duration", duration); - songMetadataMap.put("artists", artistJsonTree); + songMetadataMap.put("artists", artistNames); playlistData.add(songMetadataMap); } } @@ -419,14 +398,131 @@ private static String extractContent(HttpResponse response) { return content.toString(); } + public static String extractFilenameFromURL(String link) { // TODO: Check CLI problems on this method + // Example: "example.com/file.txt" prints "Filename detected: file.txt" + // example.com/file.json -> file.json + String file = link.substring(link.lastIndexOf("/") + 1); + if (file.isEmpty()) { + msgBroker.msgFilenameError(FILENAME_DETECTION_ERROR); + return null; + } + int index = file.lastIndexOf("."); + if (index < 0) { + msgBroker.msgFilenameError(FILENAME_DETECTION_ERROR); + return null; + } + String extension = file.substring(index); + // edge case 1: "example.com/." + if (extension.length() == 1) { + msgBroker.msgFilenameError(FILENAME_DETECTION_ERROR); + return null; + } + // file.png?width=200 -> file.png + String filename = file.split("([?])")[0]; + msgBroker.msgLogInfo(FILENAME_DETECTED + "\"" + filename + "\""); + return filename; + } + + public static HashMap getSpotifySongDownloadData(HashMap songMetadata, String directory) { + long startTime = System.currentTimeMillis(); + String songLink = songMetadata.get("link").toString(); + String songName = cleanFilename(songMetadata.get("songName").toString()); + if (songName.isEmpty()) { + songName = "Unknown_Spotify_Song_".concat(randomString(5)); + } + String filename = songName.concat(".webm"); + String downloadLink = Utility.getSpotifyDownloadLink(songMetadata); + if (downloadLink == null) { + if (Mode.isGUI()) { + msgBroker.msgLinkError("Song is exclusive to Spotify and cannot be downloaded!"); + } else { + System.out.println("\nSong (" + filename.replace(".webm", "") + ") is exclusive to Spotify and cannot be downloaded!"); + } + return null; + } + if (Mode.isGUI()) { + msgBroker.msgLinkInfo("Download link retrieved successfully!"); + } + HashMap data = new HashMap<>(); + data.put("link", songLink); + data.put("downloadLink", downloadLink); + data.put("filename", filename); + data.put("directory", directory); + msgBroker.msgLogInfo("Time taken to process Spotify song (" + filename + "): " + (System.currentTimeMillis() - startTime) + " ms"); + return data; + } + + public static String getSpotifyDownloadLink(HashMap songMetadata) { + String songName = songMetadata.get("songName").toString(); + int duration = Utility.parseStringToInt(songMetadata.get("duration").toString(), "Failed to convert Spotify song duration from String to int", MessageCategory.DOWNLOAD); + ArrayList artistNames; + if (songMetadata.get("artists") instanceof JsonArray artists) { + artistNames = new ArrayList<>(); + for (int i = 0; i < artists.size(); i++) { + artistNames.add(artists.get(i).getAsString()); + } + } else { + // Safe casting with type check + ArrayList rawList = (ArrayList) songMetadata.get("artists"); + artistNames = new ArrayList<>(); + for (Object obj : rawList) { + if (obj instanceof String) { + artistNames.add((String) obj); + } else { + // Handle the case where an element is not a String + msgBroker.msgLinkError("Failed to cast artist names to String in Spotify song metadata!"); + return null; + } + } + } + String query = (String.join(", ", artistNames) + " - " + songName).toLowerCase(); + ArrayList> searchResults = Utility.getYoutubeSearchResults(query, true); + boolean searchedWithFilters = true; + if (searchResults == null) { + msgBroker.msgLogError("Failed to get search results for the Spotify song with filters! Trying without filters ..."); + searchResults = Utility.getYoutubeSearchResults(query, false); + searchedWithFilters = false; + if (searchResults == null) { + msgBroker.msgDownloadError("Song is exclusive to Spotify and cannot be downloaded!"); + return null; + } + } + String matchedId = Utility.getMatchingVideoID(Objects.requireNonNull(searchResults), duration, artistNames); + if (matchedId.isEmpty()) { + if (searchedWithFilters) { + msgBroker.msgLogError("Failed to get a matching video ID for the song with filters! Trying without filters ..."); + searchResults = Utility.getYoutubeSearchResults(query, false); + if (searchResults == null) { + msgBroker.msgDownloadError("Song is exclusive to Spotify and cannot be downloaded!"); + return null; + } + matchedId = Utility.getMatchingVideoID(Objects.requireNonNull(searchResults), duration, artistNames); + if (matchedId.isEmpty()) { + msgBroker.msgDownloadError("Song is exclusive to Spotify and cannot be downloaded!"); + return null; + } + } else { + msgBroker.msgDownloadError("Song is exclusive to Spotify and cannot be downloaded!"); + return null; + } + } + return "https://www.youtube.com/watch?v=" + matchedId; + } + + /* + * This method is used to remove illegal characters from the filename to prevent errors while saving the file. + */ public static String cleanFilename(String filename) { String fn = StringEscapeUtils.unescapeJava(filename); + if (fn == null) { + return ""; + } return fn.replaceAll("[^a-zA-Z0-9-.%?*:|_)<(> ]+", "").strip(); } private static Runnable ytDLPJsonData(String folderPath, String link) { return () -> { - String[] command = new String[]{Program.get(YT_DLP), "--write-info-json", "--skip-download", "--restrict-filenames", "-P", folderPath, link, "-o", "yt-metadata"}; // -o flag is used to specify the output filename which is "yt-metadata.info.json" in this case + String[] command = new String[]{Program.get(YT_DLP), "--flat-playlist", "--write-info-json", "--no-clean-info-json", "--skip-download", "--compat-options", "no-youtube-unavailable-videos", "-P", folderPath, link, "-o", "yt-metadata"}; // -o flag is used to specify the output filename, which is "yt-metadata.info.json" in this case try { ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); @@ -440,18 +536,21 @@ private static Runnable ytDLPJsonData(String folderPath, String link) { while ((line = reader.readLine()) != null) { if (line.contains("ERROR") || line.contains("WARNING")) { if (line.contains("unable to extract username")) { - msgBroker.msgLinkError("The Instagram post/reel is private!"); + ytDlpErrorMessage = "The Instagram post/reel is private!"; break; } else if (line.contains("The playlist does not exist")) { - msgBroker.msgLinkError("The YouTube playlist does not exist or is private!"); + ytDlpErrorMessage = "The YouTube playlist does not exist or is private!"; break; } else if (line.contains("Video unavailable")) { - msgBroker.msgLinkError("The YouTube video is unavailable!"); + ytDlpErrorMessage = "The YouTube video is unavailable ".concat(line.substring(line.indexOf("because"))); break; } else if (line.contains("Skipping player responses from android clients")) { msgBroker.msgLogWarning(line); } else if (line.contains("Unable to download webpage") && line.contains("Temporary failure in name resolution")) { - msgBroker.msgLinkError("You are not connected to the Internet!"); + ytDlpErrorMessage = "You are not connected to the Internet!"; + break; + } else if (line.contains("unable to extract shared data; please report this issue on")) { + ytDlpErrorMessage = "Instagram post/reel is not accessible due to temporary blockage by Instagram!"; break; } else { if (line.contains("ERROR")) { @@ -462,9 +561,13 @@ private static Runnable ytDLPJsonData(String folderPath, String link) { } } } + if (ytDlpErrorMessage != null) { + msgBroker.msgLinkError(ytDlpErrorMessage); + } } } catch (Exception e) { - msgBroker.msgLinkError("Failed to get link metadata! " + e.getMessage()); + ytDlpErrorMessage = "Failed to get link metadata! " + e.getMessage(); + msgBroker.msgLinkError(ytDlpErrorMessage); } }; } @@ -522,6 +625,10 @@ public static String getMatchingVideoID(ArrayList> searc } @SuppressWarnings("unchecked") ArrayList artistsFromSearchResult = (ArrayList) searchResult.get("artists"); + if (artistsFromSearchResult == null) { + msgBroker.msgLinkError("Failed to get artists from search result!"); + continue; + } for (String artist : artistNames) { if (artistsFromSearchResult.contains(artist)) { noOfMatches++; @@ -807,18 +914,12 @@ public static Runnable setSpotifyAccessToken() { }; } - public static String getSpotifyFilename(String jsonString) { - JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); - String songName = jsonObject.get("songName").getAsString(); - return cleanFilename(songName) + ".webm"; - } - public static String convertToMp3(Path inputFilePath) { String command = Program.get(Program.FFMPEG); Path outputFilePath = inputFilePath.getParent().resolve(FilenameUtils.getBaseName(inputFilePath.toString()) + " - converted.mp3").toAbsolutePath(); String newFilename; if (outputFilePath.toFile().exists()) { - newFilename = renameFile(outputFilePath.getFileName().toString(), outputFilePath.getParent().toString()); // rename the file if it already exists else ffmpeg conversion hangs indefinitely and tries to overwrite the file + newFilename = renameFile(outputFilePath.getFileName().toString(), outputFilePath.getParent().toString()); // rename the file if it already exists, else ffmpeg conversion hangs indefinitely and tries to overwrite the file outputFilePath = outputFilePath.getParent().resolve(newFilename); } ProcessBuilder convertToMp3 = new ProcessBuilder(command, "-i", inputFilePath.toString(), outputFilePath.toString()); diff --git a/GUI/src/main/java/backend/FileDownloader.java b/GUI/src/main/java/backend/FileDownloader.java index b503ad11e..6d8bae902 100644 --- a/GUI/src/main/java/backend/FileDownloader.java +++ b/GUI/src/main/java/backend/FileDownloader.java @@ -1,8 +1,5 @@ package backend; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import gui.init.Environment; import gui.support.SplitDownloadMetrics; import gui.utils.MessageBroker; @@ -27,8 +24,6 @@ import java.nio.channels.ReadableByteChannel; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; @@ -36,17 +31,16 @@ import java.util.regex.Pattern; import static gui.support.Colors.GREEN; -import static gui.support.Colors.RED; +import static gui.support.Colors.DARK_RED; import static gui.support.Constants.*; public class FileDownloader extends Task { private static final MessageBroker M = Environment.getMessageBroker(); private static final String YT_DLP = Program.get(Program.YT_DLP); private final StringProperty progressProperty = new SimpleStringProperty(); - private String link; + private final String downloadLink; private final String filename; private final String dir; - private final String spotifySongMetadata; private final LinkType type; private int exitCode = 1; private boolean done; @@ -63,18 +57,16 @@ public class FileDownloader extends Task { public FileDownloader(Job job, StringProperty linkProperty, StringProperty dirProperty, StringProperty filenameProperty, StringProperty downloadMessage, IntegerProperty transferSpeedProperty, DoubleProperty progressProperty) { this.job = job; - this.link = job.getLink(); + this.downloadLink = job.getDownloadLink(); this.filename = Utility.cleanFilename(job.getFilename()); this.dir = job.getDir(); - this.type = LinkType.getLinkType(link); - if (this.type.equals(LinkType.SPOTIFY)) { - this.spotifySongMetadata = job.getSpotifyMetadataJson(); - } else { - this.spotifySongMetadata = null; + if (this.downloadLink == null && LinkType.getLinkType(job.getSourceLink()).equals(LinkType.SPOTIFY)) { + sendFinalMessage("Song is exclusive to Spotify and cannot be downloaded!"); } + this.type = LinkType.getLinkType(this.downloadLink); setProperties(); Platform.runLater(() -> { - linkProperty.setValue(link); + linkProperty.setValue(job.getSourceLink()); filenameProperty.setValue(filename); dirProperty.setValue(dir); progressProperty.bind(this.progressProperty()); @@ -88,14 +80,7 @@ protected Integer call() { updateProgress(0, 1); sendInfoMessage(String.format(TRYING_TO_DOWNLOAD_F, filename)); switch (type) { - case YOUTUBE, INSTAGRAM -> downloadYoutubeOrInstagram(false); - case SPOTIFY -> { - link = getSpotifyDownloadLink(spotifySongMetadata); - if (link == null) { - break; - } - downloadYoutubeOrInstagram(true); - } + case YOUTUBE, INSTAGRAM -> downloadYoutubeOrInstagram(LinkType.getLinkType(job.getSourceLink()).equals(LinkType.SPOTIFY)); case OTHER -> splitDecision(); default -> sendFinalMessage(INVALID_LINK); } @@ -105,7 +90,7 @@ protected Integer call() { } private void downloadYoutubeOrInstagram(boolean isSpotifySong) { - String[] fullCommand = new String[]{YT_DLP, "--quiet", "--progress", "-P", dir, link, "-o", filename, "-f", (isSpotifySong ? "bestaudio" : "mp4")}; + String[] fullCommand = new String[]{YT_DLP, "--quiet", "--progress", "-P", dir, downloadLink, "-o", filename, "-f", (isSpotifySong ? "bestaudio" : "mp4")}; ProcessBuilder processBuilder = new ProcessBuilder(fullCommand); sendInfoMessage(String.format(DOWNLOADING_F, filename)); Process process = null; @@ -117,11 +102,11 @@ private void downloadYoutubeOrInstagram(boolean isSpotifySong) { 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); + 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); + M.msgDownloadError(PERMISSION_DENIED_ERROR); } else if ("videounavailable".equals(messageArray[0].toLowerCase().trim().replaceAll(" ", ""))) { // If YouTube / Instagram video is unavailable - M.msgDownloadError(VIDEO_UNAVAILABLE); + M.msgDownloadError(VIDEO_UNAVAILABLE_ERROR); } else { M.msgDownloadError("An Unknown Error occurred! " + e.getMessage()); } @@ -155,7 +140,7 @@ private void downloadYoutubeOrInstagram(boolean isSpotifySong) { private void splitDecision() { long fileSize; try { - URL url = new URI(link).toURL(); + URL url = new URI(downloadLink).toURL(); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.connect(); fileSize = con.getHeaderFieldLong("Content-Length", -1); @@ -168,7 +153,7 @@ private void splitDecision() { exitCode = 1; return; } catch (IOException e) { - M.msgDownloadError(String.format(FAILED_CONNECTION_F, link)); + M.msgDownloadError(String.format(FAILED_CONNECTION_F, downloadLink)); exitCode = 1; return; } @@ -223,7 +208,7 @@ private void splitDownload() { String path = job.getFile().getAbsolutePath(); try { int numParts = new DownloadMetrics().getThreadCount(); - url = new URI(link).toURL(); + url = new URI(downloadLink).toURL(); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.connect(); long fileSize = con.getHeaderFieldLong("Content-Length", -1); @@ -297,7 +282,7 @@ private void splitDownload() { message = String.format(WRITE_ACCESS_DENIED_F, path); exitCode = 1; } catch (FileNotFoundException e) { - message = FILE_NOT_FOUND; + message = FILE_NOT_FOUND_ERROR; exitCode = 1; } catch (UnknownHostException e) { message = "You are not connected to the internet!"; @@ -317,7 +302,7 @@ private void downloadFile() { Path path = Paths.get(dir, filename); URL url = null; try { - url = new URI(link).toURL(); + url = new URI(downloadLink).toURL(); URLConnection con = url.openConnection(); con.connect(); @@ -360,7 +345,7 @@ private void downloadFile() { message = String.format(WRITE_ACCESS_DENIED_F, path); exitCode = 1; } catch (FileNotFoundException e) { - message = FILE_NOT_FOUND; + message = FILE_NOT_FOUND_ERROR; exitCode = 1; } catch (IOException e) { message = String.format(FAILED_CONNECTION_F, url); @@ -415,7 +400,7 @@ So I put a check in the second regex match (m2.find()) because if int minutes = parts.length > 1 ? Utility.parseStringToInt(parts[1], "Failed to parse minutes in ETA", MessageCategory.DOWNLOAD) : 0; int seconds = parts.length > 2 ? Utility.parseStringToInt(parts[2], "Failed to parse seconds in ETA", MessageCategory.DOWNLOAD) : 0; String time = String.format("%02d:%02d:%02d", hours, minutes, seconds); - updateMessage(speed + " " + units + " ETA " + time); + updateMessage("Downloading at " + speed + units + "/s (ETA " + time + ")"); if (progress > 99) { lastProgress = value; } @@ -436,7 +421,7 @@ private void sendFinalMessage(String message) { msg = message.isEmpty() ? String.format(SUCCESSFULLY_DOWNLOADED_F, filename) : message; M.msgDownloadInfo(msg); } else { - UIController.setDownloadInfoColor(RED); + UIController.setDownloadInfoColor(DARK_RED); msg = message.isEmpty() ? String.format(FAILED_TO_DOWNLOAD_F, filename) : message; M.msgDownloadError(msg); } @@ -451,51 +436,6 @@ public int getExitCode() { return exitCode; } - public String getSpotifyDownloadLink(String spotifyMetadataJson) { - sendInfoMessage("Trying to get download link..."); - if (spotifyMetadataJson == null) { - sendFinalMessage("Song metadata is missing!"); - return null; - } - JsonObject jsonObject = JsonParser.parseString(spotifyMetadataJson).getAsJsonObject(); - String songName = jsonObject.get("songName").getAsString(); - int duration = jsonObject.get("duration").getAsInt(); - JsonArray artists = jsonObject.get("artists").getAsJsonArray(); - ArrayList artistNames = new ArrayList<>(artists.size()); - for (int i = 0; i < artists.size(); i++) { - artistNames.add(artists.get(i).getAsString()); - } - String query = (String.join(", ", artistNames) + " - " + songName).toLowerCase(); - ArrayList> searchResults = Utility.getYoutubeSearchResults(query, true); - boolean searchedWithFilters = true; - if (searchResults == null) { - M.msgLogError("Failed to get search results for the song with filters! Trying without filters ..."); - searchResults = Utility.getYoutubeSearchResults(query, false); - searchedWithFilters = false; - if (searchResults == null) { - sendFinalMessage("Song is exclusive to Spotify and cannot be downloaded!"); - return null; - } - } - String matchedId = Utility.getMatchingVideoID(Objects.requireNonNull(searchResults), duration, artistNames); - if (matchedId.isEmpty()) { - if (searchedWithFilters) { - M.msgLogError("Failed to get a matching video ID for the song with filters! Trying without filters ..."); - searchResults = Utility.getYoutubeSearchResults(query, false); - matchedId = Utility.getMatchingVideoID(Objects.requireNonNull(searchResults), duration, artistNames); - if (matchedId.isEmpty()) { - sendFinalMessage("Song is exclusive to Spotify and cannot be downloaded!"); - return null; - } - } else { - sendFinalMessage("Song is exclusive to Spotify and cannot be downloaded!"); - return null; - } - } - sendInfoMessage("Download link retrieved successfully!"); - return "https://www.youtube.com/watch?v=" + matchedId; - } - private double parseStringToDouble(String value) { try { return Double.parseDouble(value); diff --git a/GUI/src/main/java/gui/preferences/Clear.java b/GUI/src/main/java/gui/preferences/Clear.java index 0daa6d612..15001c3ca 100644 --- a/GUI/src/main/java/gui/preferences/Clear.java +++ b/GUI/src/main/java/gui/preferences/Clear.java @@ -23,8 +23,4 @@ public void mainAutoPaste() { public void mainTheme() { preferences.remove(MAIN_THEME.toString()); } - - public void jobs() { - preferences.remove(JOBS.toString()); - } } diff --git a/GUI/src/main/java/gui/preferences/Get.java b/GUI/src/main/java/gui/preferences/Get.java index befec1b5f..58613f79c 100644 --- a/GUI/src/main/java/gui/preferences/Get.java +++ b/GUI/src/main/java/gui/preferences/Get.java @@ -3,19 +3,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import gui.support.Folders; -import gui.support.Jobs; -import org.apache.commons.io.FileUtils; import org.hildan.fxgson.FxGson; -import properties.Program; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.prefs.Preferences; import static gui.preferences.Labels.*; -import static properties.Program.JOB_FILE; public class Get extends preferences.Get { private static final Get INSTANCE = new Get(); @@ -25,7 +17,6 @@ static Get getInstance() { return INSTANCE; } - public Folders folders() { GsonBuilder gsonBuilder = new GsonBuilder(); Gson gson = FxGson.addFxSupport(gsonBuilder).setPrettyPrinting().create(); @@ -46,22 +37,6 @@ public String mainTheme() { return preferences.get(MAIN_THEME.toString(), "Light"); } - public Jobs jobs() { - GsonBuilder gsonBuilder = new GsonBuilder(); - Gson gson = FxGson.addFxSupport(gsonBuilder).setPrettyPrinting().create(); - Jobs jobs; - Path jobBatchFile = Paths.get(Program.get(JOB_FILE)); - try { - String json = FileUtils.readFileToString(jobBatchFile.toFile(), Charset.defaultCharset()); - if (json != null && !json.isEmpty()) { - jobs = gson.fromJson(json, Jobs.class); - return jobs; - } - } catch (IOException ignored) { - } - return new Jobs(); - } - public boolean alwaysAutoPaste() { return preferences.getBoolean(ALWAYS_AUTO_PASTE.toString(), false); } diff --git a/GUI/src/main/java/gui/preferences/Labels.java b/GUI/src/main/java/gui/preferences/Labels.java index 8fc0541d7..5d5d03fc3 100644 --- a/GUI/src/main/java/gui/preferences/Labels.java +++ b/GUI/src/main/java/gui/preferences/Labels.java @@ -1,5 +1,5 @@ package gui.preferences; public enum Labels implements preferences.Labels { - FOLDERS, MAIN_AUTO_PASTE, JOBS, ALWAYS_AUTO_PASTE, MAIN_THEME + FOLDERS, MAIN_AUTO_PASTE, ALWAYS_AUTO_PASTE, MAIN_THEME } diff --git a/GUI/src/main/java/gui/preferences/Set.java b/GUI/src/main/java/gui/preferences/Set.java index 862a6c4f3..824e2a7ec 100644 --- a/GUI/src/main/java/gui/preferences/Set.java +++ b/GUI/src/main/java/gui/preferences/Set.java @@ -3,19 +3,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import gui.support.Folders; -import gui.support.Jobs; -import org.apache.commons.io.FileUtils; import org.hildan.fxgson.FxGson; -import properties.Program; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.prefs.Preferences; import static gui.preferences.Labels.*; -import static properties.Program.JOB_FILE; public final class Set extends preferences.Set { private static final Set INSTANCE = new Set(); @@ -45,17 +37,4 @@ public void mainTheme(String theme) { AppSettings.CLEAR.mainTheme(); preferences.put(MAIN_THEME.toString(), theme); } - - public void jobs(Jobs jobs) { - GsonBuilder gsonBuilder = new GsonBuilder(); - Gson gson = FxGson.addFxSupport(gsonBuilder).setPrettyPrinting().create(); - String value = gson.toJson(jobs); - AppSettings.CLEAR.jobs(); - Path jobBatchFile = Paths.get(Program.get(JOB_FILE)); - try { - FileUtils.writeStringToFile(jobBatchFile.toFile(), value, Charset.defaultCharset()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } } diff --git a/GUI/src/main/java/gui/support/Colors.java b/GUI/src/main/java/gui/support/Colors.java index 7a9f37d87..bd2315b80 100644 --- a/GUI/src/main/java/gui/support/Colors.java +++ b/GUI/src/main/java/gui/support/Colors.java @@ -4,10 +4,9 @@ public final class Colors { public static final Color GREEN = Color.rgb(0, 150, 0); - public static final Color RED = Color.rgb(157, 0, 0); + public static final Color DARK_RED = Color.rgb(157, 0, 0); + public static final Color BRIGHT_RED = Color.rgb(255, 59, 48); public static final Color PURPLE = Color.rgb(125, 0, 75); public static final Color HOTPINK = Color.rgb(255, 0, 175); - public static final Color BLACK = Color.rgb(0, 0, 0); public static final Color YELLOW = Color.rgb(255, 255, 0); - public static final Color BLUE = Color.rgb(0, 100, 255); } diff --git a/GUI/src/main/java/gui/support/GUIDownloadConfiguration.java b/GUI/src/main/java/gui/support/GUIDownloadConfiguration.java new file mode 100644 index 000000000..b2b98e61b --- /dev/null +++ b/GUI/src/main/java/gui/support/GUIDownloadConfiguration.java @@ -0,0 +1,26 @@ +package gui.support; + +import gui.init.Environment; +import gui.utils.MessageBroker; +import support.DownloadConfiguration; + +public class GUIDownloadConfiguration extends DownloadConfiguration { + private final MessageBroker messageBroker = Environment.getMessageBroker(); + + public GUIDownloadConfiguration(String link, String directory, String filename) { + super(link, directory, filename); + } + + public void prepareFileData() { + messageBroker.msgLinkInfo("Fetching file data..."); + int statusCode = fetchFileData(); + if (statusCode == 0) { + messageBroker.msgLinkInfo("File data fetched successfully"); + messageBroker.msgLinkInfo("Adding file(s) to batch..."); + updateJobList(); + messageBroker.msgLinkInfo("File(s) added to batch successfully"); + } else { + messageBroker.msgLogError("Failed to fetch file data"); + } + } +} diff --git a/GUI/src/main/java/gui/utils/MessageBroker.java b/GUI/src/main/java/gui/utils/MessageBroker.java index b00fbec5c..69a1f762d 100644 --- a/GUI/src/main/java/gui/utils/MessageBroker.java +++ b/GUI/src/main/java/gui/utils/MessageBroker.java @@ -1,5 +1,6 @@ package gui.utils; +import gui.preferences.AppSettings; import javafx.scene.paint.Color; import properties.MessageCategory; import properties.MessageType; @@ -8,9 +9,7 @@ import java.util.Objects; -import static gui.support.Colors.RED; -import static gui.support.Colors.GREEN; -import static gui.support.Colors.YELLOW; +import static gui.support.Colors.*; import static properties.MessageCategory.LOG; public class MessageBroker extends utils.MessageBroker { @@ -27,7 +26,7 @@ protected void sendMessage(String message, MessageType messageType, MessageCateg ui = null; } Color color = switch (messageType) { - case ERROR -> RED; + case ERROR -> "Dark".equals(AppSettings.GET.mainTheme()) ? BRIGHT_RED : DARK_RED; case INFO -> GREEN; default -> YELLOW; }; diff --git a/GUI/src/main/java/main/Drifty_GUI.java b/GUI/src/main/java/main/Drifty_GUI.java index 53b9b8b9c..4cf1bf4a1 100644 --- a/GUI/src/main/java/main/Drifty_GUI.java +++ b/GUI/src/main/java/main/Drifty_GUI.java @@ -145,14 +145,14 @@ private Menu getHelpMenu() { bug.setOnAction(e -> openWebsite("https://github.com/SaptarshiSarkar12/Drifty/issues/new?assignees=&labels=bug+%F0%9F%90%9B%2CApp+%F0%9F%92%BB&projects=&template=Bug-for-application.yaml&title=%5BBUG%5D+")); securityVulnerability.setOnAction(e -> openWebsite("https://github.com/SaptarshiSarkar12/Drifty/security/advisories/new")); feature.setOnAction(e -> openWebsite("https://github.com/SaptarshiSarkar12/Drifty/issues/new?assignees=&labels=feature+%E2%9C%A8%2CApp+%F0%9F%92%BB&projects=&template=feature-request-application.yaml&title=%5BFEATURE%5D+")); - checkForUpdates.setOnAction(e -> { + checkForUpdates.setOnAction(e -> new Thread(() -> { if (Utility.isOffline()) { ConfirmationDialog noInternet = new ConfirmationDialog("No Internet Connection", "You are currently offline! Please check your internet connection and try again.", true, false); noInternet.getResponse(); } else { - new Thread(this::checkForUpdates).start(); + checkForUpdates(); } - }); + }).start()); about.setOnAction(event -> { if (aboutInstance == null) { aboutInstance = new About(); diff --git a/GUI/src/main/java/ui/ConfirmationDialog.java b/GUI/src/main/java/ui/ConfirmationDialog.java index 4d42b7194..229bb17fd 100644 --- a/GUI/src/main/java/ui/ConfirmationDialog.java +++ b/GUI/src/main/java/ui/ConfirmationDialog.java @@ -46,6 +46,7 @@ enum State { private Stage stage; private VBox vbox; private String filename = ""; + private TextField tfFilename; public ConfirmationDialog(String windowTitle, String message, boolean okOnly, boolean isUpdateError) { this.windowTitle = windowTitle; @@ -120,7 +121,7 @@ private void createControls() { stage.close(); }); btnOk = newButton("OK", e -> stage.close()); - TextField tfFilename = new TextField(filename); + tfFilename = new TextField(filename); tfFilename.setMinWidth(width * .4); tfFilename.setMaxWidth(width * .8); tfFilename.setPrefWidth(width * .8); @@ -167,6 +168,7 @@ private void showScene() { Theme.changeButtonStyle(isDark, btnYes); Theme.changeButtonStyle(isDark, btnNo); Theme.changeButtonStyle(isDark, btnOk); + Theme.updateTextFields(isDark, false, tfFilename); stage.setWidth(width); stage.setHeight(height); stage.setScene(scene); diff --git a/GUI/src/main/java/ui/FileMetadataRetriever.java b/GUI/src/main/java/ui/FileMetadataRetriever.java new file mode 100644 index 000000000..e8f59d8b8 --- /dev/null +++ b/GUI/src/main/java/ui/FileMetadataRetriever.java @@ -0,0 +1,58 @@ +package ui; + +import gui.support.GUIDownloadConfiguration; +import javafx.application.Platform; +import javafx.concurrent.Task; +import utils.Utility; + +import java.util.Timer; +import java.util.TimerTask; + +import static gui.support.Colors.GREEN; + +public class FileMetadataRetriever extends Task { + private final GUIDownloadConfiguration config; + // Progress bar direction + boolean dirUp = true; + + public FileMetadataRetriever(GUIDownloadConfiguration config) { + this.config = config; + } + + @Override + protected Void call() { + updateProgress(0, 1); + this.updateMessage("Retrieving Filename(s)"); + Timer progTimer = new Timer(); + progTimer.scheduleAtFixedRate(runProgress(), 0, 150); + Thread prepareFileData = new Thread(config::prepareFileData); + prepareFileData.start(); + while (config.getFileCount() == 0 && config.getStatusCode() == 0) { + Utility.sleep(100); + } + int fileCount = config.getFileCount(); + while (prepareFileData.isAlive()) { + int filesProcessed = config.getFilesProcessed(); + updateProgress(filesProcessed, fileCount); + } + UIController.setDownloadInfoColor(GREEN); + updateMessage("File(s) added to batch."); + progTimer.cancel(); + updateProgress(0, 1); + return null; + } + + private TimerTask runProgress() { + return new TimerTask() { + @Override + public void run() { + Platform.runLater(() -> { + double value = getProgress(); + dirUp = (value >= 1.0 || value <= 0.0) != dirUp; + value += dirUp ? 0.01 : -0.01; + updateProgress(value, 1.0); + }); + } + }; + } +} diff --git a/GUI/src/main/java/ui/GetFilename.java b/GUI/src/main/java/ui/GetFilename.java deleted file mode 100644 index feb9ea221..000000000 --- a/GUI/src/main/java/ui/GetFilename.java +++ /dev/null @@ -1,203 +0,0 @@ -package ui; - -import gui.init.Environment; -import javafx.application.Platform; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.concurrent.Task; -import org.apache.commons.io.FileUtils; -import properties.MessageCategory; -import properties.Program; -import support.Job; -import utils.Utility; - -import java.io.*; -import java.nio.charset.Charset; -import java.util.StringJoiner; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static gui.support.Colors.GREEN; -import static properties.Program.YT_DLP; -import static utils.Utility.isSpotify; -import static utils.Utility.sleep; - -public class GetFilename extends Task> { - private final String link; - private final String dir; - private final String regex = "(\\[download] Downloading item \\d+ of )(\\d+)"; - private final Pattern pattern = Pattern.compile(regex); - private final String lineFeed = System.lineSeparator(); - private int result = -1; - private int fileCount; - private int filesProcessed; - private final ConcurrentLinkedDeque jobList = new ConcurrentLinkedDeque<>(); - private final StringProperty feedback = new SimpleStringProperty(); - boolean dirUp = true; - - public GetFilename(String link, String dir) { - this.link = link; - this.dir = dir; - } - - @Override - protected ConcurrentLinkedDeque call() { - updateProgress(0, 1); - this.updateMessage("Retrieving Filename(s)"); - Timer progTimer = new Timer(); - progTimer.scheduleAtFixedRate(runProgress(), 2500, 150); - Thread thread = new Thread(getFileCount()); - result = -1; - thread.start(); - while (result == -1) { - sleep(500); - } - boolean proceed = true; - if (fileCount > 0) { - progTimer.cancel(); - updateProgress(0, 1); - progTimer = null; - ConfirmationDialog ask = new ConfirmationDialog("Confirmation", "There are " + fileCount + " files in this list. Proceed and get all filenames?"); - proceed = ask.getResponse().isYes(); - } - if (proceed) { - Timer timer = new Timer(); - timer.scheduleAtFixedRate(getJson(), 100, 1); - if (isSpotify(link)) { - Utility.getSpotifySongMetadata(link); - } else { - Utility.getYtDlpMetadata(link); - } - sleep(1000); // give timerTask enough time to do its last run - timer.cancel(); - jobList.clear(); - UIController.setDownloadInfoColor(GREEN); - updateMessage("File(s) added to batch."); - if (progTimer != null) { - progTimer.cancel(); - } - updateProgress(0, 1); - } - return jobList; - } - - private TimerTask getJson() { - File appFolder = Program.getJsonDataPath().toFile(); - return new TimerTask() { - @Override - public void run() { - ConcurrentLinkedDeque jobList = new ConcurrentLinkedDeque<>(); - ConcurrentLinkedDeque deleteList = new ConcurrentLinkedDeque<>(); - File[] files = appFolder.listFiles(); - if (files == null) { - return; - } - for (File file : files) { - try { - if ("yt-metadata.info.json".equals(file.getName())) { - String jsonString = FileUtils.readFileToString(file, Charset.defaultCharset()); - String filename = Utility.getFilenameFromJson(jsonString); - updateMessage("Filename detected: " + filename); - jobList.addLast(new Job(link, dir, filename, false)); - } else if ("spotify-metadata.json".equals(file.getName())) { - String jsonString = FileUtils.readFileToString(file, Charset.defaultCharset()); - String filename = Utility.getSpotifyFilename(jsonString); - updateMessage("Filename detected: " + filename); - jobList.addLast(new Job(link, dir, filename, jsonString, false)); - } else { - continue; - } - if (fileCount > 1) { - filesProcessed++; - updateProgress(filesProcessed, fileCount); - } - UIController.addJob(jobList); - deleteList.addLast(file); - } catch (IOException e) { - Environment.getMessageBroker().msgFilenameError("Failed to get filename(s) from link: " + link); - Environment.getMessageBroker().msgLogError(e.getMessage()); - } - } - for (File file : deleteList) { - try { - if (file.exists()) { - FileUtils.forceDelete(file); - } - } catch (IOException ignored) { - } - } - } - }; - } - - - private TimerTask runProgress() { - return new TimerTask() { - @Override - public void run() { - Platform.runLater(() -> { - double value = getProgress(); - value = dirUp ? value + .01 : value - .01; - if (value > 1.0) { - dirUp = !dirUp; - value = 1.0; - } - if (value < 0) { - dirUp = !dirUp; - value = 0.0; - } - updateProgress(value, 1.0); - }); - } - }; - } - - private Runnable getFileCount() { - return () -> { - feedback.addListener(((observable, oldValue, newValue) -> { - String[] list = newValue.split(lineFeed); - if (list.length > 3) { - for (String line : list) { - Matcher m = pattern.matcher(line); - if (m.find()) { - fileCount = Utility.parseStringToInt(m.group(2), "Failed to get file count", MessageCategory.FILENAME); - break; - } - } - } - })); - try { - String command = Program.get(YT_DLP); - String[] args = new String[]{command, "--flat-playlist", "--skip-download", "-P", dir, link}; - ProcessBuilder pb = new ProcessBuilder(args); - pb.redirectErrorStream(true); - Process process = pb.start(); - StringJoiner joiner = new StringJoiner(lineFeed); - try { - try ( - InputStream inputStream = process.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)) - ) - { - String line; - while ((line = reader.readLine()) != null) { - if (this.isCancelled()) { - break; - } - joiner.add(line); - feedback.setValue(joiner.toString()); - } - } - } catch (IOException e) { - Environment.getMessageBroker().msgFilenameError("Failed to get filename(s) from link: " + link); - } - result = process.waitFor(); - } catch (IOException | InterruptedException e) { - Environment.getMessageBroker().msgFilenameError("Failed to get filename(s) from link: " + link); - } - }; - } -} diff --git a/GUI/src/main/java/ui/Theme.java b/GUI/src/main/java/ui/Theme.java index 5b8bd3a3b..97acb55cf 100644 --- a/GUI/src/main/java/ui/Theme.java +++ b/GUI/src/main/java/ui/Theme.java @@ -6,6 +6,7 @@ import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.paint.Color; @@ -74,14 +75,8 @@ private static void updateTextColors(boolean isDark, Scene... scenes) { } } changeInfoTextFlow(color); - // TextFields - String style = isDark ? "-fx-text-fill: White;" : "-fx-text-fill: Black;"; - UIController.form.tfDir.setStyle(style); - UIController.form.tfFilename.setStyle(style); - UIController.form.tfLink.setStyle(style); - if (Settings.getTfCurrentDirectory() != null) { - Settings.getTfCurrentDirectory().setStyle(style + "-fx-font-weight: Bold"); - } + updateTextFields(isDark, false, UIController.form.tfDir, UIController.form.tfFilename, UIController.form.tfLink); + updateTextFields(isDark, true, Settings.getTfCurrentDirectory()); } private static void changeInfoTextFlow(Paint color) { @@ -125,6 +120,15 @@ static void changeButtonStyle(boolean isDark, Button button) { } } + static void updateTextFields(boolean isDark, boolean isBold, TextField... textFields) { + String style = isDark ? "-fx-text-fill: White;" : "-fx-text-fill: Black;"; + for (TextField textField : textFields) { + if (textField != null) { + textField.setStyle(isBold ? style.concat("-fx-font-weight: Bold") : style); + } + } + } + private static void updateCSS(boolean isDark, Scene... scenes) { for (Scene scene : scenes) { if (scene != null) { diff --git a/GUI/src/main/java/ui/UIController.java b/GUI/src/main/java/ui/UIController.java index b2f70fea0..e101ca586 100644 --- a/GUI/src/main/java/ui/UIController.java +++ b/GUI/src/main/java/ui/UIController.java @@ -1,13 +1,11 @@ package ui; import backend.FileDownloader; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import gui.init.Environment; import gui.preferences.AppSettings; import gui.support.Constants; import gui.support.Folders; -import gui.support.Jobs; +import gui.support.GUIDownloadConfiguration; import gui.updater.GUIUpdateExecutor; import gui.utils.CheckFile; import gui.utils.MessageBroker; @@ -37,6 +35,7 @@ import properties.OS; import support.Job; import support.JobHistory; +import support.Jobs; import utils.Utility; import java.io.File; @@ -44,19 +43,18 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.*; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.LinkedList; +import java.util.concurrent.*; import static gui.support.Colors.*; -import static utils.Utility.*; +import static utils.Utility.renameFile; +import static utils.Utility.sleep; public final class UIController { public static final UIController INSTANCE = new UIController(); public static MainGridPane form; private Stage helpStage; - + private GUIDownloadConfiguration downloadConfig; private static final MessageBroker M = Environment.getMessageBroker(); private static final BooleanProperty DIRECTORY_EXISTS = new SimpleBooleanProperty(false); private static final BooleanProperty PROCESSING_BATCH = new SimpleBooleanProperty(false); @@ -67,9 +65,7 @@ public final class UIController { private int speedValue; private static final TextFlow INFO_TF = new TextFlow(); private static Scene infoScene; - private String songMetadataJson; private String filename; - private String songLink; private Folders folders; private Job selectedJob; @@ -136,7 +132,7 @@ private void downloadUpdate() { getJobs().clear(); // Download the latest executable - Job updateJob = new Job(Constants.updateURL.toString(), latestExecutableFile.getParent(), latestExecutableFile.getName(), false); + Job updateJob = new Job(Constants.updateURL.toString(), latestExecutableFile.getParent(), latestExecutableFile.getName(), Constants.updateURL.toString()); addJob(updateJob); Thread downloadUpdate = new Thread(batchDownloader()); downloadUpdate.start(); @@ -169,11 +165,12 @@ private void setControlProperties() { BooleanBinding disableStartButton = form.listView.itemsProperty().isNotNull().not().or(PROCESSING_BATCH).or(DIRECTORY_EXISTS.not()).or(VERIFYING_LINKS); BooleanBinding disableInputs = PROCESSING_BATCH.or(VERIFYING_LINKS); + BooleanBinding disableLinkInput = UPDATING_BATCH.or(PROCESSING_BATCH).or(VERIFYING_LINKS); form.btnSave.visibleProperty().bind(UPDATING_BATCH); form.btnStart.disableProperty().bind(disableStartButton); form.tfDir.disableProperty().bind(disableInputs); form.tfFilename.disableProperty().bind(disableInputs); - form.tfLink.disableProperty().bind(disableInputs); + form.tfLink.disableProperty().bind(disableLinkInput); form.listView.setContextMenu(getListMenu()); if ("Dark".equals(AppSettings.GET.mainTheme())) { @@ -186,13 +183,6 @@ private void setControlProperties() { Tooltip.install(form.tfLink, new Tooltip("URL must be a valid URL without spaces." + nl + " Add multiple URLs by pasting them in from the clipboard and separating each URL with a space.")); Tooltip.install(form.tfFilename, new Tooltip("If the filename you enter already exists in the download folder, it will" + nl + "automatically be renamed to avoid file over-writes.")); Tooltip.install(form.tfDir, new Tooltip("Right click anywhere to add a new download folder." + nl + "Drifty will accumulate a list of download folders" + nl + "so that duplicate downloads can be detected.")); - form.listView.setOnMouseClicked(e -> { - Job job = form.listView.getSelectionModel().getSelectedItem(); - setLink(job.getLink()); - setDir(job.getDir()); - setFilename(job.getFilename()); - selectJob(job); - }); form.cbAutoPaste.setSelected(AppSettings.GET.mainAutoPaste()); form.tfDir.textProperty().addListener(((observable, oldValue, newValue) -> { if (!newValue.equals(oldValue)) { @@ -216,6 +206,7 @@ private void setControlProperties() { private void setControlActions() { form.btnSave.setOnAction(e -> new Thread(() -> { + UPDATING_BATCH.setValue(true); String link = getLink(); filename = getFilename(); String dir = getDir(); @@ -226,9 +217,10 @@ private void setControlActions() { } } removeJobFromList(selectedJob); - addJob(new Job(link, dir, filename, selectedJob.getSpotifyMetadataJson(), selectedJob.repeatOK())); + addJob(new Job(link, dir, filename, selectedJob.getDownloadLink())); clearLink(); clearFilename(); + setDir(folders.getDownloadFolder()); UPDATING_BATCH.setValue(false); }).start()); form.btnStart.setOnAction(e -> new Thread(() -> { @@ -247,12 +239,18 @@ private void setControlActions() { form.tfLink.setOnKeyTyped(e -> processLink()); form.listView.setOnMouseClicked(e -> { if (e.getButton().equals(MouseButton.PRIMARY) && e.getClickCount() == 1) { - Job job = form.listView.getSelectionModel().getSelectedItem(); - if (job != null) { - selectJob(job); - setLink(job.getLink()); - setDir(job.getDir()); - setFilename(job.getFilename()); + if (UPDATING_BATCH.getValue().equals(true)) { + clearControls(); + setDir(folders.getDownloadFolder()); + UPDATING_BATCH.setValue(false); + } else { + Job job = form.listView.getSelectionModel().getSelectedItem(); + if (job != null) { + selectJob(job); + setLink(job.getSourceLink()); + setDir(job.getDir()); + setFilename(job.getFilename()); + } } } }); @@ -286,23 +284,7 @@ private void processLink() { } VERIFYING_LINKS.setValue(true); for (String link : links) { - if (isSpotify(link) && link.contains("playlist")) { - M.msgFilenameInfo("Retrieving the songs from the playlist..."); - ArrayList> playlistMetadata = Utility.getSpotifyPlaylistMetadata(link); - if (playlistMetadata != null && !playlistMetadata.isEmpty()) { - for (HashMap songMetadata : playlistMetadata) { - songLink = songMetadata.get("link").toString(); - String songName = songMetadata.get("songName").toString(); - filename = cleanFilename(songName) + ".webm"; - songMetadata.remove("link"); - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - songMetadataJson = gson.toJson(songMetadata); - verifyLinksAndWaitFor(songLink); - } - } - } else { - verifyLinksAndWaitFor(link); - } + verifyLinksAndWaitFor(link); } VERIFYING_LINKS.setValue(false); clearLink(); @@ -312,9 +294,6 @@ private void processLink() { } private void verifyLinksAndWaitFor(String link) { - if (isInstagram(link)) { - link = formatInstagramLink(link); - } Thread verify = new Thread(verifyLink(link)); verify.start(); while (!verify.getState().equals(Thread.State.TERMINATED)) { @@ -331,16 +310,17 @@ private void verifyLinksAndWaitFor(String link) { */ private Runnable verifyLink(String link) { /* - When adding links to the jobList, only YouTube, Instagram and Spotify links will be put through the process of - searching the link for more than one download, in case the link happens to be a link to a playlist. This - will probably be far more common with YouTube links. + This method is called when the user pastes a link into the Link field. It checks the link to see if it is valid. + + If it is, it will then check to see if the link has been downloaded before. If it has, it will ask the user if they want to download it again. If they do, it will automatically rename the file to avoid overwriting the existing file. + If the file has not been downloaded before, it will add the link to the job list and begin the process of extracting the filename from the link. If it is a Spotify URL (song or playlist), then the final download link will be retrieved from the Spotify API. - If the link does not test positive for YouTube, Instagram or Spotify, then it is merely added to the jobList as a job - with only the link and the download folder given to the Job class. However, the Job class will take all - the text after the last forward slash in the link and set it as the filename for that job. + If the link is not valid, the user will be informed and the link field will be cleared. Users should be instructed to click through each job in the list and make sure the filename is what they - want. They can change it after clicking on the job in the list then clicking on the save button. + want. They can change it after clicking on the job in the list, then clicking on the save button. + They can deselect the job by clicking on the list again with CTRL held down. Alternatively, they can click on the Save button to save the job. + Also, they can press the DELETE key to remove the selected job from the list. */ return () -> { if (link.isEmpty()) { @@ -372,33 +352,20 @@ private Runnable verifyLink(String link) { ConfirmationDialog ask = new ConfirmationDialog(windowTitle, message, renameFile(filename, dir)); if (ask.getResponse().isYes()) { filename = ask.getFilename(); - if (isSpotify(link)) { - addJob(new Job(link, dir, filename, job.getSpotifyMetadataJson(), true)); - } else { - addJob(new Job(link, dir, filename, true)); - } - } - } else if (Utility.isExtractableLink(link)) { - if (isSpotify(link) && link.contains("playlist")) { - if (!songMetadataJson.isEmpty()) { - addJob(new Job(link, getDir(), filename, songMetadataJson, true)); - } else { - Thread getNames = new Thread(getFilenames(songLink)); - getNames.start(); - while (!getNames.getState().equals(Thread.State.TERMINATED)) { - sleep(150); - } - } - } else { - Thread getNames = new Thread(getFilenames(link)); - getNames.start(); - while (!getNames.getState().equals(Thread.State.TERMINATED)) { - sleep(150); - } + downloadConfig = new GUIDownloadConfiguration(link, dir, filename); } } else { - addJob(new Job(link, getDir())); + downloadConfig = new GUIDownloadConfiguration(link, getDir(), null); // Filename is null because it will be retrieved from the link } + downloadConfig.sanitizeLink(); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(this::commitJobListToListView, 0, 100, TimeUnit.MILLISECONDS); + Thread getNames = new Thread(getFilenames(downloadConfig)); + getNames.start(); + while (!getNames.getState().equals(Thread.State.TERMINATED)) { + sleep(150); + } + executor.shutdown(); } clearFilename(); clearLink(); @@ -407,14 +374,15 @@ private Runnable verifyLink(String link) { } else { M.msgLinkError("Link already in job list"); clearLink(); + UPDATING_BATCH.setValue(false); } }; } - private Runnable getFilenames(String link) { + private Runnable getFilenames(GUIDownloadConfiguration config) { return () -> { - // Using a Worker Task, this method gets the filename(s) from the link. - Task> task = new GetFilename(link, getDir()); + // Using a Worker Task, this method calls the FileMetadataRetriever class to retrieve all the required metadata for the file(s) to be downloaded and adds them to the jobList. + Task task = new FileMetadataRetriever(config); Platform.runLater(() -> { /* These bindings allow the Worker thread to post relevant information to the UI, including the progress bar which @@ -422,32 +390,19 @@ private Runnable getFilenames(String link) { to extract, the progress bar goes through a static animation to indicate that the program is not frozen. The controls that are bound to the thread cannot have their text updated while they are bound or else an error will be thrown and possibly the program execution halted. */ - form.lblDownloadInfo.textProperty().bind(((Worker>) task).messageProperty()); - form.pBar.progressProperty().bind(((Worker>) task).progressProperty()); + form.lblDownloadInfo.textProperty().bind(((Worker) task).messageProperty()); + form.pBar.progressProperty().bind(((Worker) task).progressProperty()); }); - - /* - This parent thread allows us to repeatedly check the Worker Task Thread for new filenames found so that we can add them - to the job batch as they are discovered. Doing this in this thread keeps the UI from appearing frozen to the user. - We use the checkHistoryAddJobs method to look for discovered filenames. If we didn't do it this way, then we would need - to wait until all filenames are discovered then add the jobs to the batch list in one action. Doing it this way - gives the user more consistent feedback of the process while it is happening. This matters when a link contains - a lot of files because each file discovered takes a while, and when there are even hundreds of files, this process - can appear to take a long time, so constant feedback for the user becomes relevant. - */ - - setLink(link); - Thread getFilenameThread = new Thread(task); - getFilenameThread.setDaemon(true); - getFilenameThread.start(); + setLink(config.getLink()); + Thread retrieveFileData = new Thread(task); + retrieveFileData.setDaemon(true); + retrieveFileData.start(); sleep(2000); form.lblDownloadInfo.setTextFill(GREEN); - while (!getFilenameThread.getState().equals(Thread.State.TERMINATED) && !getFilenameThread.getState().equals(Thread.State.BLOCKED)) { - checkHistoryAddJobs(task); + while (!retrieveFileData.getState().equals(Thread.State.TERMINATED) && !retrieveFileData.getState().equals(Thread.State.BLOCKED)) { sleep(50); } sleep(500); - checkHistoryAddJobs(task); // Check one last time clearControls(); }; } @@ -466,7 +421,7 @@ private Runnable batchDownloader() { int speed = speedValue / 5; speedValueUpdateCount = 0; speedValue = 0; - setFilenameOutput(GREEN, speed + " /s"); + setFilenameOutput(GREEN, "Speed: " + speed + " KB/s"); } } })); @@ -517,7 +472,7 @@ private String fileExists(String filename) { private boolean linkInJobList(String link) { for (Job job : getJobs().jobList()) { - if (job.getLink().equals(link)) { + if (job.getSourceLink().equals(link)) { return true; } } @@ -529,90 +484,36 @@ private void addJob(Job newJob) { for (Job job : getJobs().jobList()) { if (job.matchesLink(newJob)) { oldJob = job; + System.out.println("Old Job: " + oldJob.getFilename() + " " + oldJob.getSourceLink()); + System.out.println("New Job: " + newJob.getFilename() + " " + newJob.getSourceLink()); break; } } if (oldJob != null) { getJobs().remove(oldJob); + System.out.println("Job Removed: " + oldJob.getFilename()); } getJobs().add(newJob); + System.out.println("Job Added: " + newJob.getFilename()); commitJobListToListView(); } - public static void addJob(ConcurrentLinkedDeque list) { - /* - This takes the list passed in as argument and compares it against the jobs in the current jobList - then it only adds jobs that do not exist. - */ - for (Job job : list) { - boolean hasJob = INSTANCE.getJobs().jobList().stream().anyMatch(jb -> jb.getFilename().equals(job.getFilename())); - if (!hasJob) { - INSTANCE.addJob(job); - } - } - } - private void removeJobFromList(Job oldJob) { getJobs().remove(oldJob); commitJobListToListView(); - M.msgBatchInfo("Job Removed: " + oldJob.getLink()); + M.msgBatchInfo("Job Removed: " + oldJob.getSourceLink()); } private void updateBatch() { UPDATING_BATCH.setValue(false); if (selectedJob != null) { - Job job = new Job(getLink(), getDir(), getFilename(), selectedJob.repeatOK()); + Job job = new Job(selectedJob.getSourceLink(), getDir(), getFilename(), selectedJob.getDownloadLink()); removeJobFromList(selectedJob); addJob(job); } selectedJob = null; } - private void checkHistoryAddJobs(Worker> worker) { - String pastJobNoFile = "You have downloaded %s in the past, but the file does not exist in your download folder." + nl.repeat(2) + " Click Yes if you still wish to download this file. Otherwise, click No."; - String pastJobFileExists = "You have downloaded %s in the past, and the file exists in your download folder." + nl.repeat(2) + "It will be renamed as shown here, or you may change the filename to your liking." + nl.repeat(2) + "Clicking Yes will commit the job with the shown filename, while clicking No will not add this file to the job list."; - String fileExistsString = "This file:" + nl.repeat(2) + "%s" + nl.repeat(2) + "Exists in in the download folder." + nl.repeat(2) + "It will be renamed as shown here, or you may change the filename to your liking." + nl.repeat(2) + "Clicking Yes will commit the job with the shown filename, while clicking No will not add this file to the job list."; - Platform.runLater(() -> { - String message; - ConfirmationDialog ask = new ConfirmationDialog("", ""); - boolean addJob; - if (worker.valueProperty().get() != null) { - for (Job job : worker.valueProperty().get()) { - boolean fileExists = job.fileExists(); - boolean hasHistory = getHistory().exists(job.getLink()); - boolean existsHasHistory = fileExists && hasHistory; - boolean existsNoHistory = fileExists && !hasHistory; - boolean fileHasHistory = hasHistory && !fileExists; - if (!getJobs().jobList().contains(job)) { - if (existsHasHistory) { - message = String.format(pastJobFileExists, job.getFilename()); - ask = new ConfirmationDialog("File Already Downloaded and Exists", message, renameFile(job.getFilename(), job.getDir())); - } else if (existsNoHistory) { - message = String.format(fileExistsString, job.getFilename()); - ask = new ConfirmationDialog("File Already Exists", message, false, false); - } else if (fileHasHistory) { - message = String.format(pastJobNoFile, job.getFilename()); - ask = new ConfirmationDialog("File Already Downloaded", message, false, false); - } - if (fileHasHistory || existsHasHistory || existsNoHistory) { - addJob = ask.getResponse().isYes(); - if (addJob) { - String newFilename = ask.getFilename(); - boolean repeatDownload = newFilename.equals(job.getFilename()); - String filename = newFilename.isEmpty() ? job.getFilename() : newFilename; - if (!filename.isEmpty()) { - addJob(new Job(job.getLink(), job.getDir(), filename, repeatDownload)); - } - } - } else { - addJob(job); - } - } - } - } - }); - } - private void delayFolderSave(String folderString, File folder) { /* If the user is typing a file path into the field, we don't want to save every folder 'hit' so we wait 3 seconds @@ -646,6 +547,7 @@ private ContextMenu getListMenu() { commitJobListToListView(); clearLink(); clearFilename(); + UPDATING_BATCH.setValue(false); form.listView.getItems().clear(); form.listView.getItems(); M.msgLinkInfo(""); @@ -738,7 +640,7 @@ public void setLinkOutput(Color color, String message) { Platform.runLater(() -> { form.lblLinkOut.getStyleClass().clear(); form.lblLinkOut.setText(message); - if (color.equals(RED) || color.equals(YELLOW)) { + if (color.equals(DARK_RED) || color.equals(YELLOW)) { new Thread(() -> { sleep(5000); clearLinkOutput(); @@ -759,7 +661,7 @@ public void setDirOutput(Color color, String message) { } form.lblDirOut.setTextFill(color); form.lblDirOut.setText(message); - if (color.equals(RED) || color.equals(YELLOW)) { + if (color.equals(DARK_RED) || color.equals(YELLOW)) { new Thread(() -> { sleep(5000); clearDirOutput(); @@ -780,7 +682,7 @@ public void setFilenameOutput(Color color, String message) { } form.lblFilenameOut.setTextFill(color); form.lblFilenameOut.setText(message); - if (color.equals(RED) || color.equals(YELLOW)) { + if (color.equals(DARK_RED) || color.equals(YELLOW)) { new Thread(() -> { sleep(5000); clearFilenameOutput(); @@ -803,7 +705,7 @@ public void setDownloadOutput(Color color, String message) { form.lblDownloadInfo.textProperty().unbind(); form.lblDownloadInfo.setTextFill(color); form.lblDownloadInfo.setText(message); - if (color.equals(RED) || color.equals(YELLOW)) { + if (color.equals(DARK_RED) || color.equals(YELLOW)) { new Thread(() -> { sleep(5000); clearDownloadOutput(); @@ -842,13 +744,9 @@ private void commitJobListToListView() { if (getJobs().isEmpty()) { form.listView.getItems().clear(); } else { - // Use TreeSet to remove duplicates and sort simultaneously - Set sortedJobs = new TreeSet<>(Comparator.comparing(Job::toString)); - sortedJobs.addAll(getJobs().jobList()); - getJobs().setList(new ConcurrentLinkedDeque<>(sortedJobs)); + // Assign the jobList to the ListView + form.listView.getItems().setAll(getJobs().jobList()); } - // Assign the jobList to the ListView - form.listView.getItems().setAll(getJobs().jobList()); } }); } diff --git a/GUI/src/main/resources/META-INF/native-image/reflect-config.json b/GUI/src/main/resources/META-INF/native-image/reflect-config.json index b9158f6e8..0baa09268 100644 --- a/GUI/src/main/resources/META-INF/native-image/reflect-config.json +++ b/GUI/src/main/resources/META-INF/native-image/reflect-config.json @@ -251,6 +251,10 @@ "name":"java.util.concurrent.atomic.Striped64", "fields":[{"name":"base"}, {"name":"cellsBusy"}] }, +{ + "name":"java.util.concurrent.atomic.Striped64$Cell", + "fields":[{"name":"value"}] +}, { "name":"javafx.scene.Camera" }, @@ -505,6 +509,11 @@ "allDeclaredFields":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"support.Jobs", + "allDeclaredFields":true, + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"ui.Splash", "methods":[{"name":"","parameterTypes":[] }]