diff --git a/.gitignore b/.gitignore
index 163374c55..4d48ad30c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ Website/.next
Website/node_modules
### IntelliJ IDEA ###
+.idea/
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
diff --git a/.idea/misc.xml b/.idea/misc.xml
index dd5e3ad2d..0b7a8fa9e 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1ddfb..dcb6b8c4c 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -3,4 +3,4 @@
-
\ No newline at end of file
+
diff --git a/config/resource-config-linux.json b/config/resource-config-linux.json
index f54bdcb96..df185a4bd 100644
--- a/config/resource-config-linux.json
+++ b/config/resource-config-linux.json
@@ -1,7 +1,8 @@
{
"resources": {
"includes": [
- {"pattern": "yt-dlp"}
+ {"pattern": "yt-dlp"},
+ {"pattern": "spotdl_linux"}
]
}
}
\ No newline at end of file
diff --git a/config/resource-config-mac.json b/config/resource-config-mac.json
index 44fb0decb..b385e6351 100644
--- a/config/resource-config-mac.json
+++ b/config/resource-config-mac.json
@@ -1,7 +1,8 @@
{
"resources": {
"includes": [
- {"pattern": "yt-dlp_macos"}
+ {"pattern": "yt-dlp_macos"},
+ {"pattern": "spotdl_macos"}
]
}
}
diff --git a/config/resource-config-windows.json b/config/resource-config-windows.json
index 4b78857ce..c4d0c1dfc 100644
--- a/config/resource-config-windows.json
+++ b/config/resource-config-windows.json
@@ -1,7 +1,8 @@
{
"resources": {
"includes": [
- {"pattern": "yt-dlp.exe"}
+ {"pattern": "yt-dlp.exe"},
+ {"pattern": "spotdl_win.exe"}
]
}
}
diff --git a/src/main/java/Backend/CopyExecutables.java b/src/main/java/Backend/CopyExecutables.java
new file mode 100644
index 000000000..823c2770d
--- /dev/null
+++ b/src/main/java/Backend/CopyExecutables.java
@@ -0,0 +1,50 @@
+package Backend;
+
+import Enums.Program;
+import Utils.Environment;
+import Utils.MessageBroker;
+import org.apache.commons.io.FileUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class CopyExecutables {
+ private static final MessageBroker M = Environment.getMessageBroker();
+
+ public void copyExecutables(String[] executableNames) throws IOException {
+ for (String executableName : executableNames) {
+ InputStream inputStream = ClassLoader.getSystemResourceAsStream(executableName);
+ Path executablePath = Program.getExecutablesPath(executableName);
+ if (!Files.exists(executablePath)) {
+ if (!executablePath.toFile().getParentFile().exists()) {
+ FileUtils.createParentDirectories(executablePath.toFile());
+ }
+ try (OutputStream outputStream = Files.newOutputStream(executablePath)) {
+ if (inputStream != null) {
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ }
+ if (!Files.isExecutable(executablePath)) {
+ ProcessBuilder makeExecutable = new ProcessBuilder("chmod", "+x", executablePath.toString());
+ makeExecutable.inheritIO();
+ Process chmod = makeExecutable.start();
+ chmod.waitFor();
+ }
+ } catch (FileAlreadyExistsException e) {
+ M.msgLogWarning(executableName + " not copied to " + Program.get(Program.DRIFTY_PATH) + " because it already exists!");
+ } catch (InterruptedException e) {
+ M.msgLogWarning("Failed to make " + executableName + " executable: " + e.getMessage());
+ } catch (IOException e) {
+ M.msgInitError("Failed to copy " + executableName + " executable: " + e.getMessage());
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/Backend/CopyYtDLP.java b/src/main/java/Backend/CopyYtDLP.java
deleted file mode 100644
index 9acca194b..000000000
--- a/src/main/java/Backend/CopyYtDLP.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package Backend;
-
-import Enums.Program;
-import Utils.Environment;
-import Utils.MessageBroker;
-import org.apache.commons.io.FileUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-public class CopyYtDLP {
- private static final MessageBroker M = Environment.getMessageBroker();
-
- public boolean copyYtDLP(InputStream inputStream) throws IOException {
- Path ytDLPPath = Program.getYtDLPFullPath();
- if (!Files.exists(ytDLPPath)) {
- if (!ytDLPPath.toFile().getParentFile().exists()) {
- FileUtils.createParentDirectories(ytDLPPath.toFile());
- }
- try (OutputStream outputStream = Files.newOutputStream(ytDLPPath)) {
- if (inputStream != null) {
- byte[] buffer = new byte[4096];
- int bytesRead;
- while ((bytesRead = inputStream.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- }
- }
- if (!Files.isExecutable(ytDLPPath)) {
- ProcessBuilder makeExecutable = new ProcessBuilder("chmod", "+x", ytDLPPath.toString());
- makeExecutable.inheritIO();
- Process yt_dlp = makeExecutable.start();
- yt_dlp.waitFor();
- }
- } catch (FileAlreadyExistsException e) {
- M.msgLogWarning("yt-dlp not copied to " + Program.get(Program.DRIFTY_PATH) + " because it already exists!");
- } catch (InterruptedException e) {
- M.msgLogWarning("Failed to make yt-dlp executable: " + e.getMessage());
- } catch (IOException e) {
- M.msgInitError("Failed to copy yt-dlp executable: " + e.getMessage());
- }
- }
- return Files.exists(ytDLPPath);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/Backend/FileDownloader.java b/src/main/java/Backend/FileDownloader.java
index 34bf7167b..11d3ee6ed 100644
--- a/src/main/java/Backend/FileDownloader.java
+++ b/src/main/java/Backend/FileDownloader.java
@@ -27,7 +27,7 @@ public class FileDownloader implements Runnable {
private final long threadMaxDataSize;
private final String dir;
private String fileName;
- private final String link;
+ private String link;
private URL url;
public FileDownloader(String link, String fileName, String dir) {
@@ -57,14 +57,6 @@ public String getDir() {
}
}
- public String getLink() {
- return link;
- }
-
- public String getFileName() {
- return fileName;
- }
-
private void downloadFile() {
try {
ReadableByteChannel readableByteChannel;
@@ -134,7 +126,7 @@ public void downloadFromYouTube() {
fileDownloadMessage = outputFileName;
}
M.msgDownloadInfo("Trying to download \"" + fileDownloadMessage + "\" ...");
- ProcessBuilder processBuilder = new ProcessBuilder(Program.get(YT_DLP), "--quiet", "--progress", "-P", dir, link, "-o", outputFileName, "-f", "mp4");
+ ProcessBuilder processBuilder = new ProcessBuilder(Program.get(YT_DLP), "--quiet", "--progress", "-P", dir, link, "-o", outputFileName);
processBuilder.inheritIO();
M.msgDownloadInfo(String.format(DOWNLOADING_F, fileDownloadMessage));
int exitValueOfYt_Dlp = -1;
@@ -191,8 +183,18 @@ public boolean mergeDownloadedFileParts(List fileOutputStreams
@Override
public void run() {
+ 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";
+ }
+ }
boolean isYouTubeLink = isYoutube(link);
boolean isInstagramLink = isInstagram(link);
+ boolean isSpotifyLink = isSpotify(link);
try {
// If the link is of a YouTube or Instagram video, then the following block of code will execute.
if (isYouTubeLink || isInstagramLink) {
@@ -207,6 +209,8 @@ public void run() {
} catch (Exception e) {
if (isYouTubeLink) {
M.msgDownloadError(YOUTUBE_DOWNLOAD_FAILED);
+ } else if (isSpotifyLink) {
+ M.msgDownloadError(SPOTIFY_DOWNLOAD_FAILED);
} else {
M.msgDownloadError(INSTAGRAM_DOWNLOAD_FAILED);
}
diff --git a/src/main/java/CLI/Main.java b/src/main/java/CLI/Main.java
index 9c9296e71..73bd078df 100644
--- a/src/main/java/CLI/Main.java
+++ b/src/main/java/CLI/Main.java
@@ -32,6 +32,7 @@ public class Main {
protected static JobHistory jobHistory;
protected static boolean isYoutubeURL;
protected static boolean isInstagramLink;
+ protected static boolean isSpotifyLink;
protected static boolean isInstagramImage;
private static MessageBroker messageBroker;
private static String link;
@@ -41,7 +42,7 @@ public class Main {
private static boolean batchDownloading;
private static String batchDownloadingFile;
private static final String msg_FileExists_NoHistory = "\"%s\" exists in \"%s\" folder. It will be renamed to \"%s\".";
- private static final String msg_FileExists_HasHistory = "You have previously downloaded \"%s\" and it exists in \"%s\" folder. Do you want to download it again? ";
+ private static final String msg_FileExists_HasHistory = "You have previously downloaded \"%s\" and it exists in \"%s\" folder.\nDo you want to download it again? ";
public static void main(String[] args) {
logger.log(MessageType.INFO, CLI_APPLICATION_STARTED);
@@ -76,7 +77,6 @@ public static void main(String[] args) {
} else {
if (isURL(args[i])) {
link = args[i];
-
} else {
messageBroker.msgInitError("Invalid argument(s) passed!");
System.exit(1);
@@ -88,14 +88,11 @@ public static void main(String[] args) {
boolean isUrlValid;
if (Utility.isURL(link)) {
isUrlValid = Utility.isLinkValid(link);
-
} else {
isUrlValid = false;
messageBroker.msgLinkError("Link is invalid!");
}
if (isUrlValid) {
- isYoutubeURL = isYoutube(link);
- isInstagramLink = isInstagram(link);
if (name == null) {
if (fileName == null || fileName.isEmpty()) {
messageBroker.msgFilenameInfo("Retrieving filename from link...");
@@ -151,6 +148,7 @@ public static void main(String[] args) {
downloadsFolder = getProperDownloadsFolder(downloadsFolder);
isYoutubeURL = isYoutube(link);
isInstagramLink = isInstagram(link);
+ isSpotifyLink = isSpotify(link);
messageBroker.msgFilenameInfo("Retrieving filename from link...");
fileName = findFilenameInLink(link);
Job job = new Job(link, downloadsFolder, fileName, false);
@@ -227,6 +225,7 @@ private static void batchDownloader() {
messageBroker.msgLinkInfo("[" + (i + 1) + "/" + numberOfLinks + "] " + "Processing link : " + link);
isYoutubeURL = isYoutube(link);
isInstagramLink = isInstagram(link);
+ isSpotifyLink = isSpotify(link);
if (data.containsKey("fileNames") && !data.get("fileNames").get(i).isEmpty()) {
fileName = data.get("fileNames").get(i);
} else {
@@ -254,7 +253,7 @@ 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)) {
+ if ((fileName == null || (fileName.isEmpty())) && (!isYoutubeURL && !isInstagramLink && !isSpotifyLink)) {
System.out.print(ENTER_FILE_NAME_WITH_EXTENSION);
if (removeInputBufferFirst) {
SC.nextLine();
@@ -340,19 +339,24 @@ private static void checkHistoryAddJobsAndDownload(Job job) {
fileName = Utility.renameFile(fileName, downloadsFolder);
System.out.printf(msg_FileExists_NoHistory + "\n", job.getFilename(), job.getDir(), fileName);
renameFilenameIfRequired(true);
+ if (isSpotifyLink) {
+ link = Utility.getSpotifyDownloadLink(link);
+ }
job = new Job(link, downloadsFolder, fileName, false);
jobHistory.addJob(job,true);
FileDownloader downloader = new FileDownloader(link, fileName, downloadsFolder);
downloader.run();
} else if (fileExists_HasHistory) {
System.out.printf(msg_FileExists_HasHistory, job.getFilename(), job.getDir());
- SC.nextLine(); // to remove whitespace from input buffer
String choiceString = SC.nextLine().toLowerCase();
boolean choice = utility.yesNoValidation(choiceString, String.format(msg_FileExists_HasHistory, job.getFilename(), job.getDir()));
if (choice) {
fileName = Utility.renameFile(fileName, downloadsFolder);
System.out.println("New file name : " + fileName);
renameFilenameIfRequired(false);
+ if (isSpotifyLink) {
+ link = Utility.getSpotifyDownloadLink(link);
+ }
job = new Job(link, downloadsFolder, fileName, false);
jobHistory.addJob(job,true);
FileDownloader downloader = new FileDownloader(link, fileName, downloadsFolder);
@@ -361,6 +365,9 @@ private static void checkHistoryAddJobsAndDownload(Job job) {
} else {
jobHistory.addJob(job, true);
renameFilenameIfRequired(true);
+ if (isSpotifyLink) {
+ link = Utility.getSpotifyDownloadLink(link);
+ }
FileDownloader downloader = new FileDownloader(link, fileName, downloadsFolder);
downloader.run();
}
diff --git a/src/main/java/Enums/LinkType.java b/src/main/java/Enums/LinkType.java
index 620bda05e..da04bdcaf 100644
--- a/src/main/java/Enums/LinkType.java
+++ b/src/main/java/Enums/LinkType.java
@@ -3,7 +3,7 @@
import Utils.Utility;
public enum LinkType {
- YOUTUBE, INSTAGRAM, OTHER;
+ YOUTUBE, INSTAGRAM, SPOTIFY, OTHER;
public static LinkType getLinkType(String link) {
if (Utility.isYoutube(link)) {
@@ -12,6 +12,9 @@ public static LinkType getLinkType(String link) {
else if (Utility.isInstagram(link)) {
return INSTAGRAM;
}
+ else if (Utility.isSpotify(link)) {
+ return SPOTIFY;
+ }
else {
return OTHER;
}
@@ -21,6 +24,7 @@ public String get() {
return switch(this) {
case YOUTUBE -> "YouTube";
case INSTAGRAM -> "Instagram";
+ case SPOTIFY -> "Spotify";
case OTHER -> "Other";
};
}
diff --git a/src/main/java/Enums/Program.java b/src/main/java/Enums/Program.java
index 2c9c5b5bb..65265809c 100644
--- a/src/main/java/Enums/Program.java
+++ b/src/main/java/Enums/Program.java
@@ -2,32 +2,44 @@
import java.nio.file.Path;
import java.nio.file.Paths;
+
public enum Program {
- EXECUTABLE_NAME, YT_DLP, DRIFTY_PATH, JOB_HISTORY_FILE, JOB_FILE;
+ YT_DLP_EXECUTABLE_NAME, SPOTDL_EXECUTABLE_NAME, YT_DLP, DRIFTY_PATH, JOB_HISTORY_FILE, JOB_FILE, SPOTDL;
private static String ytDLP;
private static String driftyPath;
+ private static String spotdl;
- public static void setExecutableName(String name) {
+ public static void setYt_DlpExecutableName(String name) {
Program.ytDLP = name;
}
+ public static void setSpotdlExecutableName(String name) {
+ Program.spotdl = name;
+ }
public static void setDriftyPath(String path) { Program.driftyPath = path;}
public static String get(Program program) {
return switch (program) {
- case EXECUTABLE_NAME -> ytDLP;
+ case YT_DLP_EXECUTABLE_NAME -> ytDLP;
+ case SPOTDL_EXECUTABLE_NAME -> spotdl;
case DRIFTY_PATH -> driftyPath;
case YT_DLP -> Paths.get(driftyPath, ytDLP).toAbsolutePath().toString();
+ case SPOTDL -> Paths.get(driftyPath, spotdl).toAbsolutePath().toString();
case JOB_HISTORY_FILE -> Paths.get(driftyPath, "JobHistory.json").toAbsolutePath().toString();
case JOB_FILE -> Paths.get(driftyPath, "Jobs.json").toAbsolutePath().toString();
};
}
- public static Path getYtDLPFullPath() {
- return Paths.get(driftyPath, ytDLP);
+ public static Path getExecutablesPath(String executableName) {
+ return Paths.get(driftyPath, executableName).toAbsolutePath();
}
+
public static Path getJsonDataPath() {
return Paths.get(driftyPath, "JsonData");
}
-}
+
+ public static Path getSpotdlDataPath() {
+ return Paths.get(driftyPath, "JsonData/metafile.spotdl");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/GUI/Forms/AskYesNo.java b/src/main/java/GUI/Forms/AskYesNo.java
index 6c2bc6fa5..449473d45 100644
--- a/src/main/java/GUI/Forms/AskYesNo.java
+++ b/src/main/java/GUI/Forms/AskYesNo.java
@@ -131,10 +131,10 @@ private void createControls() {
vbox = new VBox(30, text);
vbox.setAlignment(Pos.CENTER);
vbox.setPadding(new Insets(20));
- if(state.equals(FILENAME)) {
+ if (state.equals(FILENAME)) {
vbox.getChildren().add(tfFilename);
}
- switch(state) {
+ switch (state) {
case OK -> hbox.getChildren().add(btnOk);
case YES_NO, FILENAME -> hbox.getChildren().addAll(btnYes, btnNo);
}
diff --git a/src/main/java/GUI/Forms/DownloadFile.java b/src/main/java/GUI/Forms/DownloadFile.java
index 799ffbbfd..37c76ffee 100644
--- a/src/main/java/GUI/Forms/DownloadFile.java
+++ b/src/main/java/GUI/Forms/DownloadFile.java
@@ -20,6 +20,7 @@
import java.net.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
+
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedList;
@@ -33,8 +34,9 @@
public class DownloadFile extends Task {
private static final MessageBroker M = Environment.getMessageBroker();
private final String YT_DLP = Program.get(Program.YT_DLP);
+ private final String SPOTDL = Program.get(Program.SPOTDL);
private final StringProperty progressProperty = new SimpleStringProperty();
- private final String link;
+ private String link;
private final String filename;
private final String dir;
private final LinkType type;
@@ -43,6 +45,13 @@ public class DownloadFile extends Task {
private final Job job;
private final AtomicLong totalTransferred = new AtomicLong();
private final AtomicLong totalSpeedValue = new AtomicLong();
+ private static final String regex1 = "([0-9.]+)%";
+ private static final Pattern p1 = Pattern.compile(regex1);
+ private static final String regex2 = "(\\d+\\.\\d+)([a-zA-Z/]+) ETA ([0-9:]+)";
+ private static final Pattern p2 = Pattern.compile(regex2);
+ private int updateCount = 0;
+ private double speedSum = 0.0;
+ private double lastProgress;
public DownloadFile(Job job,
StringProperty linkProperty, StringProperty dirProperty, StringProperty filenameProperty,
@@ -71,6 +80,10 @@ protected Integer call() {
sendInfoMessage(String.format(TRYING_TO_DOWNLOAD_F, filename));
switch (type) {
case YOUTUBE, INSTAGRAM -> downloadYoutubeOrInstagram();
+ case SPOTIFY -> {
+ link = getSpotifyDownloadLink(link);
+ splitDecision();
+ }
case OTHER -> splitDecision();
}
updateProgress(0.0, 1.0);
@@ -224,7 +237,7 @@ private void splitDownload() {
long totalBytes = totalTransferred.get();
String totalDownloaded = UnitConverter.format(totalBytes, 2);
double bitsTransferred = (double) totalSpeedValue.get() / seconds;
- String msg = "Downloading " + totalSize + " at " + UnitConverter.format(bitsTransferred, 2) + "/s (Total: " + totalDownloaded + ")";
+ String msg = "Downloading at " + UnitConverter.format(bitsTransferred, 2) + "/s (Downloaded " + totalDownloaded + " out of " + totalSize + ")";
updateMessage(msg);
totalSpeedValue.set(0);
}
@@ -232,7 +245,7 @@ private void splitDownload() {
}
updateProgress(0.0, 1.0);
updateMessage("Merging Files");
- String msg = "Saving file to download folder .";
+ String msg = "Saving file to download folder";
FileOutputStream fos = new FileOutputStream(job.getFile());
long position = 0;
for (int i = 0; i < numParts; i++) {
@@ -299,7 +312,7 @@ private void downloadFile() {
start = end;
String totalDownloaded = UnitConverter.format(totalBytesRead, 2);
double bitsTransferred = bytesInTime / 10 / seconds;
- String msg = "Downloading " + totalSize + " at " + UnitConverter.format(bitsTransferred * 100, 2) + "/s (Total: " + totalDownloaded + ")";
+ String msg = "Downloading at " + UnitConverter.format(bitsTransferred, 2) + "/s (Downloaded " + totalDownloaded + " out of " + totalSize + ")";
updateMessage(msg);
bytesInTime = 0;
}
@@ -330,14 +343,6 @@ private void downloadFile() {
sendFinalMessage(message);
}
- private static final String regex1 = "([0-9.]+)%";
- private static final Pattern p1 = Pattern.compile(regex1);
- private static final String regex2 = "(\\d+\\.\\d+)([a-zA-Z/]+) ETA ([0-9:]+)";
- private static final Pattern p2 = Pattern.compile(regex2);
- private int updateCount = 0;
- private double speedSum = 0.0;
- private double lastProgress;
-
private void setProperties() {
progressProperty.addListener(((observable, oldValue, newValue) -> {
Matcher m1 = p1.matcher(newValue);
@@ -346,9 +351,9 @@ private void setProperties() {
double progress = 0.0;
if (m1.find()) {
/*
- Because yt-dlp throws its progress numbers all over the place to where
+ As yt-dlp throws its progress numbers all over the place to where
sometimes it's lower than it was the previous time, causing the
- progress bar bounce forward and backward during the download, which
+ progress bar to bounce forward and backward during the download, which
did not look proper since progress bars are only supposed to go up.
In order to minimize this as much as possible, I used the
@@ -357,7 +362,7 @@ private void setProperties() {
stick at that max number.
So I put a check in the second regex match (m2.find()) because if
- we are still matching then the file is still downloading, and it
+ we are still matching, then the file is still downloading, and it
checks the value of progress and if it's too high, then it sets the
lastProgress back to the number that makes sense.
*/
@@ -382,8 +387,9 @@ So I put a check in the second regex match (m2.find()) because if
int seconds = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
String time = String.format("%02d:%02d:%02d", hours, minutes, seconds);
updateMessage(speed + " " + units + " ETA " + time);
- if (progress > 99)
+ if (progress > 99) {
lastProgress = value;
+ }
}
}
}));
@@ -399,8 +405,7 @@ private void sendFinalMessage(String message) {
if (exitCode == 0) {
msg = message.isEmpty() ? String.format(SUCCESSFULLY_DOWNLOADED_F, filename) : message;
M.msgDownloadInfo(msg);
- }
- else {
+ } else {
msg = message.isEmpty() ? String.format(FAILED_TO_DOWNLOAD_F, filename) : message;
M.msgDownloadError(msg);
}
@@ -414,4 +419,33 @@ public boolean notDone() {
public int getExitCode() {
return exitCode;
}
+
+ public String getSpotifyDownloadLink(String link){
+ sendInfoMessage("Trying to get download link for \"" + link + "\"");
+ String spotDLPath = Program.get(Program.SPOTDL);
+ ProcessBuilder processBuilder = new ProcessBuilder(spotDLPath, "url", link);
+ Process process;
+ try {
+ process = processBuilder.start();
+ } catch (IOException e) {
+ exitCode = 1;
+ sendFinalMessage("Failed to get download link for \"" + link + "\"!");
+ return null;
+ }
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(process).getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (!line.contains("Processing query:") && line.startsWith("http")) {
+ sendInfoMessage("Download link retrieved successfully!");
+ exitCode = 0;
+ return line;
+ }
+ }
+ } catch (IOException e) {
+ exitCode = 1;
+ sendFinalMessage("Failed to get download link for \"" + link + "\"!");
+ return null;
+ }
+ return null;
+ }
}
diff --git a/src/main/java/GUI/Forms/GUI_Logic.java b/src/main/java/GUI/Forms/GUI_Logic.java
index 24577e7bd..caf88b0a8 100644
--- a/src/main/java/GUI/Forms/GUI_Logic.java
+++ b/src/main/java/GUI/Forms/GUI_Logic.java
@@ -225,11 +225,11 @@ private void processLink() {
private Runnable verifyLink(String link) {
/*
- When adding links to the jobList, only YouTube and Instagram links will be put through the process of
+ 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.
- If the link does not test positive for YouTube or Instagram, then it is merely added to the jobList as a job
+ 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.
@@ -250,35 +250,35 @@ private Runnable verifyLink(String link) {
Job job = getHistory().getJob(link);
filename = job.getFilename();
dir = getDir();
- if(dir == null) {
- System.err.println("dir is null");
+ if (dir == null) {
+ M.msgDirError("Download folder is not set!");
System.exit(0);
}
String intro = "You have downloaded this link before. The filename is:" + nl + filename + nl.repeat(2);
String folder = fileExists(filename);
+ String windowTitle;
if (!folder.isEmpty()) {
message = intro + "And the file exists in this download folder:" + nl + folder + nl.repeat(2) +
"If you wish to download it again, it will be given the name shown below, or you can change it as you wish." + nl.repeat(2) +
"YES will add the job to the list with new filename. NO will do nothing.";
- }
- else {
+ windowTitle = "File Already Exists";
+ } else {
message = intro + "However, the file does not exist in any of your download folders." + nl.repeat(2) +
"Do you still wish to download this file?";
+ windowTitle = "File Already Downloaded";
}
- AskYesNo ask = new AskYesNo(message, renameFile(filename, dir));
+ AskYesNo ask = new AskYesNo(windowTitle, message, renameFile(filename, dir));
if (ask.getResponse().isYes()) {
filename = ask.getFilename();
addJob(new Job(link, dir, filename, true));
}
- }
- else if (Utility.isExtractableLink(link)) {
+ } else if (Utility.isExtractableLink(link)) {
Thread getNames = new Thread(getFilenames(link));
getNames.start();
while (!getNames.getState().equals(Thread.State.TERMINATED)) {
sleep(150);
}
- }
- else {
+ } else {
addJob(new Job(link, getDir()));
}
}
diff --git a/src/main/java/GUI/Forms/GetFilename.java b/src/main/java/GUI/Forms/GetFilename.java
index ea6ff73d9..b19f26752 100644
--- a/src/main/java/GUI/Forms/GetFilename.java
+++ b/src/main/java/GUI/Forms/GetFilename.java
@@ -22,6 +22,7 @@
import java.util.regex.Pattern;
import static Enums.Program.YT_DLP;
+import static Utils.Utility.*;
public class GetFilename extends Task> {
private final String link;
@@ -29,17 +30,18 @@ public class GetFilename extends Task> {
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 = 0;
private int filesProcessed = 0;
+
public GetFilename(String link, String dir) {
this.link = link;
this.dir = dir;
}
-
private final ConcurrentLinkedDeque jobList = new ConcurrentLinkedDeque<>();
+
@Override
protected ConcurrentLinkedDeque call() {
updateProgress(0, 1);
@@ -68,10 +70,19 @@ protected ConcurrentLinkedDeque call() {
sleep(500); //give timerTask enough time to do its last run
jobList.clear();
for (String json : jsonList) {
- String filename = Utility.getFilenameFromJson(json);
- String fileLink = Utility.getURLFromJson(json);
- String baseName = FilenameUtils.getBaseName(filename);
- filename = baseName + ".mp4";
+ String filename;
+ String fileLink;
+ if (isSpotify(link)) {
+ filename = Utility.extractSpotifyFilename(json);
+ fileLink = Utility.getSpotifyDownloadLink(link);
+ String baseName = FilenameUtils.getBaseName(filename);
+ filename = baseName + ".mp3";
+ } else {
+ filename = Utility.getFilenameFromJson(json);
+ fileLink = Utility.getSpotifyDownloadLink(link);
+ String baseName = FilenameUtils.getBaseName(filename);
+ filename = baseName + ".mp4";
+ }
jobList.addLast(new Job(fileLink, dir, filename, false));
}
GUI_Logic.setDownloadInfoColor(Colors.GREEN);
@@ -94,13 +105,19 @@ public void run() {
for (File file : files) {
try {
String ext = FilenameUtils.getExtension(file.getAbsolutePath());
- if (ext.equalsIgnoreCase("json")) {
+ if (ext.equalsIgnoreCase("json") || ext.equalsIgnoreCase("spotdl")) {
String jsonString = FileUtils.readFileToString(file, Charset.defaultCharset());
- String filename = Utility.getFilenameFromJson(jsonString);
- String baseName = FilenameUtils.getBaseName(filename);
- filename = baseName + ".mp4";
+ String filename;
+ if (isSpotify(link)) {
+ filename = Utility.extractSpotifyFilename(jsonString);
+ String baseName = FilenameUtils.getBaseName(filename);
+ filename = baseName + ".mp3";
+ } else {
+ filename = Utility.getFilenameFromJson(jsonString);
+ String baseName = FilenameUtils.getBaseName(filename);
+ filename = baseName + ".mp4";
+ }
updateMessage("Found file: " + filename);
- String link = Utility.getURLFromJson(jsonString);
jobList.addLast(new Job(link, dir, filename, false));
if (fileCount > 1) {
filesProcessed++;
@@ -109,8 +126,7 @@ public void run() {
GUI_Logic.addJob(jobList);
deleteList.addLast(file);
}
- } catch (IOException ignored) {
- }
+ } catch (IOException ignored) {}
}
for (File file : deleteList) {
try {
@@ -122,9 +138,9 @@ public void run() {
}
};
}
-
boolean dirUp = true;
+
private TimerTask runProgress() {
return new TimerTask() {
@Override
@@ -145,9 +161,9 @@ public void run() {
}
};
}
-
private final StringProperty feedback = new SimpleStringProperty();
+
private Runnable getFileCount() {
return () -> {
feedback.addListener(((observable, oldValue, newValue) -> {
@@ -193,8 +209,6 @@ private Runnable getFileCount() {
};
}
- private int result = -1;
-
private void sleep(long time) {
try {
TimeUnit.MILLISECONDS.sleep(time);
@@ -202,4 +216,4 @@ private void sleep(long time) {
throw new RuntimeException(e);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/GUI/Forms/GetResponse.java b/src/main/java/GUI/Forms/GetResponse.java
index e88a89468..5bf9c6e30 100644
--- a/src/main/java/GUI/Forms/GetResponse.java
+++ b/src/main/java/GUI/Forms/GetResponse.java
@@ -11,7 +11,7 @@ enum State {
private State answer = LIMBO;
public boolean isYes() {
- while(answer.equals(LIMBO)) {
+ while (answer.equals(LIMBO)) {
sleep(200);
}
return answer.equals(YES);
diff --git a/src/main/java/GUI/Forms/Main.java b/src/main/java/GUI/Forms/Main.java
index 2792df70f..40a2d82de 100644
--- a/src/main/java/GUI/Forms/Main.java
+++ b/src/main/java/GUI/Forms/Main.java
@@ -37,7 +37,6 @@ public static void main(String[] args) {
Environment.setMessageBroker(M);
M.msgLogInfo(DriftyConstants.GUI_APPLICATION_STARTED);
Environment.initializeEnvironment();
- Utility.setStartTime();
launch(args);
}
@@ -128,11 +127,11 @@ private Menu getHelpMenu() {
MenuItem securityVulnerability = new MenuItem("Report a Security Vulnerability");
MenuItem feature = new MenuItem("Suggest a Feature");
MenuItem about = new MenuItem("About Drifty");
- contactUs.setOnAction(e -> openWebsite("https://saptarshisarkar12.github.io/Drifty/contact.html"));
+ contactUs.setOnAction(e -> openWebsite("https://saptarshisarkar12.github.io/Drifty/contact"));
contribute.setOnAction(e -> openWebsite("https://github.com/SaptarshiSarkar12/Drifty"));
- bug.setOnAction(e -> openWebsite("https://github.com/SaptarshiSarkar12/Drifty/issues/new?assignees=&labels=bug%2CApp&template=Bug-for-application.yaml&title=%5BBUG%5D+"));
+ 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=enhancement%2CApp&template=feature-request-application.yaml&title=%5BFEATURE%5D+"));
+ 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+"));
about.setOnAction(event -> {
Stage stage = Constants.getStage("About Drifty", false);
VBox root = new VBox(10);
diff --git a/src/main/java/Utils/DriftyConstants.java b/src/main/java/Utils/DriftyConstants.java
index 640dcfdb2..2ccb65356 100644
--- a/src/main/java/Utils/DriftyConstants.java
+++ b/src/main/java/Utils/DriftyConstants.java
@@ -33,7 +33,7 @@ private DriftyConstants() {}
public static final String LOCATION_FLAG_SHORT = "-l";
public static final String BATCH_FLAG_SHORT = "-b";
public static final String ENTER_FILE_NAME_WITH_EXTENSION = "Please enter the filename with file extension (filename.extension) : ";
- public static final String ENTER_FILE_LINK = "Enter the link to the file (in the form of https://www.example.com/filename.extension) or a YouTube/Instagram Video link : ";
+ public static final String ENTER_FILE_LINK = "Enter the link to the file (in the form of https://www.example.com/filename.extension) or a YouTube/Instagram Video link or Spotify link : ";
public static final String USER_HOME_PROPERTY = "user.home";
public static final String QUIT_OR_CONTINUE = "Enter Q to Quit Or any other key to Continue";
public static final String FAILED_TO_RETRIEVE_DEFAULT_DOWNLOAD_FOLDER = "Failed to retrieve default download folder!";
@@ -50,6 +50,7 @@ private DriftyConstants() {}
public static final String FAILED_READING_STREAM = "Failed to get I/O operations channel to read from the data stream !";
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 SPOTIFY_DOWNLOAD_FAILED = "Failed to download Spotify audio!";
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.";
diff --git a/src/main/java/Utils/Environment.java b/src/main/java/Utils/Environment.java
index f4585c6ea..6b673e80d 100644
--- a/src/main/java/Utils/Environment.java
+++ b/src/main/java/Utils/Environment.java
@@ -1,13 +1,12 @@
package Utils;
-import Backend.CopyYtDLP;
+import Backend.CopyExecutables;
import Enums.OS;
import Enums.Program;
import Preferences.AppSettings;
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
@@ -25,16 +24,18 @@ public class Environment {
public static void initializeEnvironment() {
M.msgLogInfo("OS : " + OS.getOSName());
String ytDLP = OS.isWindows() ? "yt-dlp.exe" : OS.isMac() ? "yt-dlp_macos" : "yt-dlp";
+ String spotdl = OS.isWindows() ? "spotdl_win.exe" : OS.isMac() ? "spotdl_macos" : "spotdl_linux";
String appUseFolderPath = OS.isWindows() ? Paths.get(System.getenv("LOCALAPPDATA"), "Drifty").toAbsolutePath().toString() : Paths.get(System.getProperty("user.home"), ".config", "Drifty").toAbsolutePath().toString();
- Program.setExecutableName(ytDLP);
+ Program.setYt_DlpExecutableName(ytDLP);
+ Program.setSpotdlExecutableName(spotdl);
Program.setDriftyPath(appUseFolderPath);
- InputStream ytDLPStream = ClassLoader.getSystemResourceAsStream(ytDLP);
- CopyYtDLP copyYtDlp = new CopyYtDLP();
+ CopyExecutables copyExecutables = new CopyExecutables();
boolean ytDLPExists = false;
try {
- ytDLPExists = copyYtDlp.copyYtDLP(ytDLPStream);
+ copyExecutables.copyExecutables(new String[]{ytDLP, spotdl});
} catch (IOException e) {
M.msgInitError("Failed to copy yt-dlp! " + e.getMessage());
+ M.msgInitError("Failed to copy spotDL! " + e.getMessage());
}
if (ytDLPExists && !isYtDLPUpdated()) {
updateYt_dlp();
@@ -71,13 +72,11 @@ public static void updateYt_dlp() {
M.msgInitError("Component (yt-dlp) update process was interrupted! " + e.getMessage());
}
}
-
public static boolean isYtDLPUpdated() {
final long oneDay = 1000 * 60 * 60 * 24; // Value of one day (24 Hours) in milliseconds
long timeSinceLastUpdate = System.currentTimeMillis() - AppSettings.get.lastDLPUpdateTime();
return timeSinceLastUpdate <= oneDay;
}
-
public static MessageBroker getMessageBroker() {
return M;
}
diff --git a/src/main/java/Utils/Utility.java b/src/main/java/Utils/Utility.java
index ff97d9165..31217a00b 100644
--- a/src/main/java/Utils/Utility.java
+++ b/src/main/java/Utils/Utility.java
@@ -13,8 +13,10 @@
import org.buildobjects.process.ProcBuilder;
import org.hildan.fxgson.FxGson;
+import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.net.*;
import java.nio.charset.Charset;
import java.nio.file.Path;
@@ -27,6 +29,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import static Enums.Program.SPOTDL;
import static Enums.Program.YT_DLP;
import static Utils.DriftyConstants.*;
@@ -36,15 +39,7 @@ public final class Utility {
private static final Scanner SC = ScannerFactory.getInstance();
private static boolean interrupted;
-
- public static void setStartTime() {
- }
-
- public Utility() {
- }
-
public static boolean isYoutube(String url) {
-
String pattern = "^(http(s)?://)?((w){3}.)?youtu(be|.be)?(\\.com)?/.+";
return url.matches(pattern);
}
@@ -53,9 +48,13 @@ public static boolean isInstagram(String url) {
String pattern = "(https?://(?:www\\.)?instagr(am|.am)?(\\.com)?/(p|reel)/([^/?#&]+)).*";
return url.matches(pattern);
}
+ public static boolean isSpotify(String url) {
+ String pattern = "(https?://(open.spotify\\.com|play\\.spotify\\.com)/(track|album|playlist)/[a-zA-Z0-9]+).*";
+ return url.matches(pattern);
+ }
public static boolean isExtractableLink(String link) {
- return isYoutube(link) || isInstagram(link);
+ return isYoutube(link) || isInstagram(link) || isSpotify(link);
}
public static boolean isLinkValid(String link) {
@@ -98,7 +97,16 @@ public static String findFilenameInLink(String link) {
if (isInstagram(link) || isYoutube(link)) {
LinkedList linkMetadataList = Utility.getLinkMetadata(link);
for (String json : Objects.requireNonNull(linkMetadataList)) {
- filename = Utility.getFilenameFromJson(json);
+ filename = Utility.getFilenameFromJson(json);
+ }
+ } else if (isSpotify(link)) {
+ LinkedList linkMetadataList = Utility.getLinkMetadata(link);
+ File file = Program.getSpotdlDataPath().toFile();
+ try {
+ String json = FileUtils.readFileToString(file, Charset.defaultCharset());
+ filename = Utility.extractSpotifyFilename(json);
+ } catch(IOException e){
+ M.msgFilenameError(AUTO_FILE_NAME_DETECTION_FAILED);
}
} else {
// Example: "example.com/file.txt" prints "Filename detected: file.txt"
@@ -150,7 +158,6 @@ public boolean yesNoValidation(String input, String printMessage) {
System.out.print(printMessage);
input = SC.nextLine().toLowerCase();
}
-
char choice = input.charAt(0);
if (choice == 'y') {
return true;
@@ -177,27 +184,30 @@ public static LinkedList getLinkMetadata(String link) {
M.msgLinkError("Failed to create temporary directory for Drifty to get link metadata!");
return null;
}
- Thread linkThread = new Thread(ytDLPJsonData(driftyJsonFolder.getAbsolutePath(), link));
+ Thread linkThread;
+ if (isSpotify(link)){
+ driftyJsonFolder = Program.getSpotdlDataPath().toFile();
+ linkThread = new Thread(spotdlJsonData(driftyJsonFolder.getAbsolutePath(), link));
+ } else {
+ linkThread = new Thread(ytDLPJsonData(driftyJsonFolder.getAbsolutePath(), link));
+ }
linkThread.start();
while (!linkThread.getState().equals(Thread.State.TERMINATED) && !linkThread.isInterrupted()) {
sleep(100);
interrupted = linkThread.isInterrupted();
}
-
if (interrupted) {
FileUtils.forceDelete(driftyJsonFolder);
return null;
}
-
File[] files = driftyJsonFolder.listFiles();
if (files != null) {
for (File file : files) {
String ext = FilenameUtils.getExtension(file.getAbsolutePath());
- if (ext.toLowerCase().contains("json")) {
+ if (ext.toLowerCase().contains("json")||ext.toLowerCase().contains("spotdl")) {
String linkMetadata = FileUtils.readFileToString(file, Charset.defaultCharset());
list.addLast(linkMetadata);
}
-
}
FileUtils.forceDelete(driftyJsonFolder); // delete the metadata files of Drifty from the config directory
}
@@ -208,18 +218,6 @@ public static LinkedList getLinkMetadata(String link) {
}
}
- public static String getURLFromJson(String jsonString) {
- String json = makePretty(jsonString);
- String regexLink = "(\"webpage_url\": \")(.+)(\")";
- String extractedUrl = "";
- Pattern p = Pattern.compile(regexLink);
- Matcher m = p.matcher(json);
- if (m.find()) {
- extractedUrl = StringEscapeUtils.unescapeJava(m.group(2));
- }
- return extractedUrl;
- }
-
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();
@@ -258,6 +256,22 @@ public static String getFilenameFromJson(String jsonString) {
return filename;
}
+ public static String extractSpotifyFilename(String spotdlData) {
+ String json = makePretty(spotdlData);
+ String filename;
+ String regex = "(\"name\": \")(.+)(\",)";
+ Pattern p = Pattern.compile(regex);
+ Matcher m = p.matcher(json);
+ if (m.find()) {
+ filename = cleanFilename(m.group(2)) + ".mp3";
+ M.msgFilenameInfo(FILENAME_DETECTED + "\"" + filename + "\"");
+ } else {
+ filename = cleanFilename("Unknown_Filename_") + randomString(15) + ".mp3";
+ M.msgFilenameError(AUTO_FILE_NAME_DETECTION_FAILED_YT_IG);
+ }
+ return filename;
+ }
+
public static String cleanFilename(String filename) {
String fn = StringEscapeUtils.unescapeJava(filename);
return fn.replaceAll("[^a-zA-Z0-9-._)<(> ]+", "").strip();
@@ -275,6 +289,17 @@ private static Runnable ytDLPJsonData(String folderPath, String link) {
};
}
+ private static Runnable spotdlJsonData(String folderPath, String link) {
+ return () -> {
+ String command = Program.get(SPOTDL);
+ String[] args = new String[]{"save", link, "--save-file", folderPath};
+ new ProcBuilder(command)
+ .withArgs(args)
+ .withErrorStream(System.err)
+ .withNoTimeout()
+ .run();
+ };
+ }
public static boolean isURL(String text) {
String regex = "^(http(s)?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";
Pattern p = Pattern.compile(regex);
@@ -282,8 +307,6 @@ public static boolean isURL(String text) {
return m.matches();
}
-
-
public static void sleep(long time) {
try {
TimeUnit.MILLISECONDS.sleep(time);
@@ -302,4 +325,30 @@ public static String randomString(int characterCount) {
}
return sb.toString();
}
+
+ public static String getSpotifyDownloadLink(String link){
+ M.msgDownloadInfo("Trying to get download link for \"" + link + "\"");
+ String spotDLPath = Program.get(Program.SPOTDL);
+ ProcessBuilder processBuilder = new ProcessBuilder(spotDLPath, "url", link);
+ Process process;
+ try {
+ process = processBuilder.start();
+ } catch (IOException e) {
+ M.msgDownloadError("Failed to get download link for \"" + link + "\"!");
+ return null;
+ }
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(process).getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (!line.contains("Processing query:") && line.startsWith("http")) {
+ M.msgDownloadInfo("Download link retrieved successfully!");
+ return line;
+ }
+ }
+ } catch (IOException e) {
+ M.msgDownloadError("Failed to get download link for \"" + link + "\"!");
+ return null;
+ }
+ return null;
+ }
}
diff --git a/src/main/resources/spotdl_linux b/src/main/resources/spotdl_linux
new file mode 100644
index 000000000..8136c9319
Binary files /dev/null and b/src/main/resources/spotdl_linux differ
diff --git a/src/main/resources/spotdl_macos b/src/main/resources/spotdl_macos
new file mode 100644
index 000000000..ef73f453a
Binary files /dev/null and b/src/main/resources/spotdl_macos differ
diff --git a/src/main/resources/spotdl_win.exe b/src/main/resources/spotdl_win.exe
new file mode 100644
index 000000000..33532825b
Binary files /dev/null and b/src/main/resources/spotdl_win.exe differ