Skip to content

Commit

Permalink
feat: Add support for downloading Spotify songs (#293)
Browse files Browse the repository at this point in the history
* feat: Added support for downloading Spotify songs

* fix: Changed executable name for Spotify in linux and merged copy executables classes

* fix: fixed missing variable in Environment class

* Added progress bar

* added changes to update progress bar

* fix: Fixed merge conflicts and broken support for Spotify filename detection in Drifty CLI

* chore: Added spotdl executables to resource config

---------

Co-authored-by: Saptarshi Sarkar <saptarshi.programmer@gmail.com>
  • Loading branch information
sinchana06 and SaptarshiSarkar12 authored Oct 21, 2023
1 parent 9a6cdb3 commit 398720d
Show file tree
Hide file tree
Showing 24 changed files with 299 additions and 170 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Website/.next
Website/node_modules

### IntelliJ IDEA ###
.idea/
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
Expand Down
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion config/resource-config-linux.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"resources": {
"includes": [
{"pattern": "yt-dlp"}
{"pattern": "yt-dlp"},
{"pattern": "spotdl_linux"}
]
}
}
3 changes: 2 additions & 1 deletion config/resource-config-mac.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"resources": {
"includes": [
{"pattern": "yt-dlp_macos"}
{"pattern": "yt-dlp_macos"},
{"pattern": "spotdl_macos"}
]
}
}
3 changes: 2 additions & 1 deletion config/resource-config-windows.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"resources": {
"includes": [
{"pattern": "yt-dlp.exe"}
{"pattern": "yt-dlp.exe"},
{"pattern": "spotdl_win.exe"}
]
}
}
50 changes: 50 additions & 0 deletions src/main/java/Backend/CopyExecutables.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
}
48 changes: 0 additions & 48 deletions src/main/java/Backend/CopyYtDLP.java

This file was deleted.

24 changes: 14 additions & 10 deletions src/main/java/Backend/FileDownloader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -57,14 +57,6 @@ public String getDir() {
}
}

public String getLink() {
return link;
}

public String getFileName() {
return fileName;
}

private void downloadFile() {
try {
ReadableByteChannel readableByteChannel;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -191,8 +183,18 @@ public boolean mergeDownloadedFileParts(List<FileOutputStream> 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) {
Expand All @@ -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);
}
Expand Down
21 changes: 14 additions & 7 deletions src/main/java/CLI/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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...");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/Enums/LinkType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
}
Expand All @@ -21,6 +24,7 @@ public String get() {
return switch(this) {
case YOUTUBE -> "YouTube";
case INSTAGRAM -> "Instagram";
case SPOTIFY -> "Spotify";
case OTHER -> "Other";
};
}
Expand Down
24 changes: 18 additions & 6 deletions src/main/java/Enums/Program.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Loading

1 comment on commit 398720d

@vercel
Copy link

@vercel vercel bot commented on 398720d Oct 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.