├── .gitignore ├── README.md ├── Spotify Downloader.exe ├── guitool2.png ├── lib └── downloader.js ├── main.js ├── package.json └── spot_node.tar.gz /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify Playlist Downloader With Windows GUI 2 | 3 | ![image](guitool2.png) 4 | 5 | ## IMPORTANT NOTICE THE TOOL IS NOT WORKING. DOWNLOADING IT WOULD BE USELESS. WORKING ON A UPDATE. 6 | 7 | ## 12/MAY Still working on the update.... sorry. 8 | 9 | Download an entire spotify playlist (160kbps mp3's) to your local machine with a simple interface 10 | 11 | When it starts downloading it checks if you already downloaded the song if so it skips it. 12 | When done downloading it also writes the ID3 data and album art to the file. 13 | 14 | ###To install: 15 | Install nodejs (USE INSTALLER!! Install 64bit if you're on a 64bit system!!!) if you haven't already. ([NodeJS Downloads](https://nodejs.org/en/download/)) 16 | Microsoft .NET Framework 4.6 if you haven't already. ([Download](https://www.microsoft.com/en-US/download/details.aspx?id=48130)) 17 | 18 | Then download this repository ([HERE](https://github.com/dekiller82/spotify-playlist-downloader-with-windows-gui/archive/master.zip)) or use the button on github. 19 | 20 | Unpack the repository and run the .exe (source code can be found [here]( https://github.com/dekiller82/Spotify-Playlist-Downloader-GUI)) 21 | 22 | ###First Time Setup (Only needs to be done right after downloading) 23 | 24 | **I recommend using a brand new Spotify Account for this tool!** 25 | 26 | Log In using your Spotify Username and Password. 27 | 28 | You can't login with Facebook accounts so you will have to create a new Spotify user to login. 29 | 30 | The tool will check for the node_modules folder on startup. If it's not there it will take care of the npm install. 31 | 32 | ###How To Get Playlist URL 33 | 34 | [CLICK HERE FOR A STEP BY STEP GUIDE WITH PICTURES](http://imgur.com/a/tAFo3) 35 | 36 | ###Changelog 37 | 38 | #####V2.0 39 | 40 | **RELAUNCH! IF YOU ALREADY HAVE V1.2 OR BELOW FULLY REINSTALL THIS REPOSITORY** 41 | 42 | New UI 43 | 44 | Bugfix for Usernames with a dot in their name 45 | 46 | New Log in screen 47 | 48 | Password will be wiped on log out only 49 | 50 | #####V1.2 51 | 52 | **IF YOU ALREADY HAVE V1.1.6 OR BELOW PLEASE FULLY REINSTALL THIS REPOSITORY** 53 | 54 | Tool now also downloads album art and attaches it. 55 | 56 | You can now download up to 5 playlists in 1 go 57 | 58 | Instead of opening a new prompt for downloading output is now in the program 59 | 60 | #####V1.1.6 61 | 62 | Bugfix for underscores in usernames 63 | 64 | #####V1.1.5 65 | 66 | Added security feature to wipe password after download. 67 | 68 | #####V1.1 69 | 70 | You can now download up to 3 playlists in 1 go 71 | 72 | #####V1.0.6 73 | 74 | NodeJS launch bugfix 75 | 76 | #####V1.0.5 77 | 78 | Added option for /Artist/Album file structure 79 | 80 | #####V1.0.1 81 | 82 | Updated default download folder to the Windows Music folder. 83 | 84 | Updated Layout 85 | 86 | #####V1 87 | 88 | Initial Release 89 | 90 | ###TO-DO 91 | 92 | Add option to set download folder (For now songs will be saved to: C:\Users\youruser\Music) 93 | 94 | ~~Add option to download multiple playlists~~ DONE! Since V1.1 95 | 96 | ~~Add option to download all mp3's to a single folder~~ DONE! Since V1.0.5 with the Artist/Album option 97 | 98 | ###Additional Comments 99 | 100 | If you get any errors please go to "C:\Program Files\nodejs" and see if node.exe and npm.cmd are installed there 101 | 102 | To download Albums for now the only way to do it is to add the songs to a playlist and then download that playlist. 103 | 104 | Free Accounts get limited after a while though, but starts downloading again afterwards 105 | 106 | You can't login with Facebook accounts so you will have to create a new Spotify user to login. 107 | 108 | #####Thanks to /u/dva010/ 109 | 110 | If you are trying to download an artist that has a '.' at the end of their name, it will create a folder that Windows will not allow you to delete without running a command in cmd. 111 | 112 | Pasted command below on how to remove the folder if you guys run into this issue. 113 | 114 | Command to delete folder that ends in '.' 115 | 116 | rd /s "\?\C:\Documents and Settings\User\Desktop\Annoying Folder." 117 | 118 | ### Disclaimer: 119 | 120 | - This was done purely as an academic exercise. 121 | - This my first coding project so code is sloppy en ugly 122 | - I do not recommend you doing this illegally or against Spotify's terms of service. 123 | 124 | -------------------------------------------------------------------------------- /Spotify Downloader.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dekiller82/spotify-playlist-downloader-with-windows-gui/0da540b0ef00f8c484718ba4184a0de8088ca617/Spotify Downloader.exe -------------------------------------------------------------------------------- /guitool2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dekiller82/spotify-playlist-downloader-with-windows-gui/0da540b0ef00f8c484718ba4184a0de8088ca617/guitool2.png -------------------------------------------------------------------------------- /lib/downloader.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | (function () { 3 | var Downloader, Error, EventEmitter, Log, Path, Playlist, SpotifyWeb, Success, Track, async, colors, domain, id3, fs, lodash, mkdirp, program, util, 4 | bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; }, 5 | extend = function (child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 6 | hasProp = {}.hasOwnProperty; 7 | 8 | require('coffee-script'); 9 | 10 | fs = require('fs'); 11 | 12 | async = require('async'); 13 | 14 | lodash = require('lodash'); 15 | 16 | util = require('util'); 17 | 18 | colors = require('colors'); 19 | 20 | SpotifyWeb = require('spotify-web'); 21 | 22 | mkdirp = require('mkdirp'); 23 | 24 | Path = require('path'); 25 | 26 | program = require('commander'); 27 | 28 | id3 = require('node-id3'); 29 | 30 | domain = require('domain'); 31 | 32 | EventEmitter = require('events').EventEmitter; 33 | 34 | Error = (function (_this) { 35 | return function (err) { 36 | console.log(("" + err).red); 37 | return process.exit(1); 38 | }; 39 | })(this); 40 | 41 | Success = (function (_this) { 42 | return function (success) { 43 | console.log(("" + success).green); 44 | return process.exit(0); 45 | }; 46 | })(this); 47 | 48 | Log = (function (_this) { 49 | return function (msg) { 50 | return console.log((" - " + msg).green); 51 | }; 52 | })(this); 53 | 54 | Track = (function (superClass) { 55 | extend(Track, superClass); 56 | 57 | function Track(trackId, Spotify, directory, isMakeFolder, cb1, track1) { 58 | this.trackId = trackId; 59 | this.Spotify = Spotify; 60 | this.directory = directory; 61 | this.isMakeFolder = isMakeFolder; 62 | this.cb = cb1; 63 | this.track = track1 != null ? track1 : {}; 64 | this.writeMetaData = bind(this.writeMetaData, this); 65 | this.downloadFile = bind(this.downloadFile, this); 66 | this.createDirs = bind(this.createDirs, this); 67 | this.getTrack = bind(this.getTrack, this); 68 | this.downloadCover = bind(this.downloadCover, this); 69 | this.getTrack(); 70 | } 71 | 72 | Track.prototype.getTrack = function () { 73 | return this.Spotify.get(this.trackId, (function (_this) { 74 | return function (err, track) { 75 | if (err) { 76 | _this.invalid = true; 77 | if (!err.toString().includes("404")) 78 | _this.cb(err); 79 | 80 | _this.cb(); 81 | } 82 | else { 83 | return _this.Spotify.recurseAlternatives(track, "DE", (function (_err, newTrack) { 84 | if (_err) { 85 | _this.cb(); 86 | } 87 | else { 88 | _this.trackId = newTrack.uri; 89 | return _this.Spotify.get(newTrack.uri, (function (__this) { 90 | return function (__err, _newTrack) { 91 | if (__err) { 92 | __this.cb(); 93 | } 94 | else { 95 | __this.track = _newTrack; 96 | return __this.createDirs(); 97 | } 98 | } 99 | })(_this)); 100 | } 101 | })); 102 | }; 103 | } 104 | })(this)); 105 | }; 106 | 107 | function fixFilename(filepath) { 108 | return filepath.replace(/[/\\?%*:|"<>]/g, ''); 109 | } 110 | 111 | Track.prototype.createDirs = function () { 112 | var albumpath, artistpath, dir, filepath, stats; 113 | dir = Path.resolve("" + this.directory); 114 | if (this.isMakeFolder) { 115 | var artist_var = " "; 116 | if (this.track.artist[0] != undefined) 117 | artist_var = this.track.artist[0].name; 118 | filepath = dir + '/' + fixFilename(artist_var.replace(/\//g, ' - ')) + ' - ' + fixFilename(this.track.name.replace(/\//g, ' - ')) + '.mp3'; 119 | } 120 | else { 121 | artistpath = dir + '/' + fixFilename(this.track.artist[0].name.replace(/\//g, ' - ')) + '/'; 122 | albumpath = artistpath + fixFilename(this.track.album.name.replace(/\//g, ' - ')) + ' [' + this.track.album.date.year + ']/'; 123 | filepath = albumpath + fixFilename(this.track.artist[0].name.replace(/\//g, ' - ')) + ' - ' + fixFilename(this.track.name.replace(/\//g, ' - ')) + '.mp3'; 124 | } 125 | if (fs.existsSync(filepath)) { 126 | stats = fs.statSync(filepath); 127 | if (stats.size !== 0) { 128 | console.log(("Already Downloaded: " + this.track.artist[0].name + " " + this.track.name).yellow); 129 | return this.cb(); 130 | } 131 | } 132 | if (this.isMakeFolder) { 133 | if (!fs.existsSync(dir)) { 134 | mkdirp.sync(dir); 135 | } 136 | } 137 | else { 138 | if (!fs.existsSync(albumpath)) { 139 | mkdirp.sync(albumpath); 140 | } 141 | } 142 | this.downloadCover(filepath); 143 | return this.downloadFile(filepath); 144 | }; 145 | 146 | Track.prototype.downloadFile = function (filepath) { 147 | var d, out, failed; 148 | Log("Downloading: " + this.track.artist[0].name + " - " + this.track.name); 149 | d = domain.create(); 150 | d.on('error', (function (_this) { 151 | return function (err) { 152 | if (err.toString().indexOf("Rate limited") > -1) { 153 | failed = true; 154 | out.end(); 155 | return null; 156 | } 157 | console.log((" - - " + (err.toString()) + " ... { Skipping Track }").red); 158 | return _this.cb(); 159 | }; 160 | })(this)); 161 | return d.run((function (_this) { 162 | return function () { 163 | out = fs.createWriteStream(filepath); 164 | return _this.track.play().pipe(out).on('finish', function () { 165 | if (failed) { 166 | fs.unlinkSync(filepath); 167 | setTimeout(function () { _this.downloadFile(filepath) }, 10000); 168 | console.log((" - - Rate limited ... { Retrying in 10 seconds }").red); 169 | return null; 170 | } 171 | Log(" - DONE: " + _this.track.artist[0].name + " - " + _this.track.name); 172 | return _this.writeMetaData(filepath); 173 | }); 174 | }; 175 | })(this)); 176 | }; 177 | 178 | Track.prototype.downloadCover = function (filepath) { 179 | var httpreq = require('httpreq'); 180 | var coverFilepath = filepath + ".jpg"; 181 | var t = this.track; 182 | var url = this.Spotify.sourceUrls.LARGE + (t.album.coverGroup.image[2] ? t.album.coverGroup.image[2].uri : '').replace("undefined",""); 183 | httpreq.download( 184 | url, 185 | coverFilepath, 186 | function (err, progress) { 187 | if (err) return console.log(err); 188 | }, function (err, respose) { 189 | if (err) return console.log(err); 190 | Log("Cover Downloaded: " + t.artist[0].name + " - " + t.name); 191 | }); 192 | return this.cb; 193 | }; 194 | 195 | Track.prototype.writeMetaData = function (filepath) { 196 | 197 | var meta = { 198 | artist: this.track.artist[0].name, 199 | album: this.track.album.name, 200 | title: this.track.name, 201 | year: this.track.album.date.year.toString(), 202 | trackNumber: this.track.number.toString(), 203 | image: filepath + ".jpg" 204 | } 205 | 206 | id3.write(meta, filepath); 207 | fs.unlink(meta.image); 208 | return this.cb(); 209 | }; 210 | 211 | return Track; 212 | 213 | })(EventEmitter); 214 | 215 | Downloader = (function (superClass) { 216 | extend(Downloader, superClass); 217 | 218 | function Downloader(username, password, playlist, directory) { 219 | this.username = username; 220 | this.password = password; 221 | this.playlist = playlist; 222 | this.directory = directory; 223 | this.processTrack = bind(this.processTrack, this); 224 | this.processTracks = bind(this.processTracks, this); 225 | this.getPlaylist = bind(this.getPlaylist, this); 226 | this.attemptLogin = bind(this.attemptLogin, this); 227 | this.run = bind(this.run, this); 228 | this.Spotify = null; 229 | this.Tracks = []; 230 | this.dir = this.directory; 231 | this.makeFolder = false; 232 | this.generatePlaylist = false; 233 | } 234 | 235 | Downloader.prototype.run = function () { 236 | console.log('Downloader App Started..'.green); 237 | return async.series([this.attemptLogin, this.getPlaylist, this.processTracks], (function (_this) { 238 | return function (err, res) { 239 | if (err) { 240 | return Error("" + (err.toString())); 241 | } 242 | 243 | return Success(' ------- DONE ALL ------- '); 244 | }; 245 | })(this)); 246 | }; 247 | 248 | Downloader.prototype.attemptLogin = function (cb) { 249 | return SpotifyWeb.login(this.username, this.password, (function (_this) { 250 | return function (err, SpotifyInstance) { 251 | if (err) { 252 | return Error("Error logging in... (" + err + ")"); 253 | } 254 | _this.Spotify = SpotifyInstance; 255 | return typeof cb === "function" ? cb() : void 0; 256 | }; 257 | })(this)); 258 | }; 259 | 260 | Downloader.prototype.getPlaylist = function (cb) { 261 | Log('Getting Playlist Data'); 262 | return this.Spotify.playlist(this.playlist, 0, 9001, (function (_this) { 263 | return function (err, playlistData) { 264 | if (err) { 265 | return Error("Playlist data error... " + err); 266 | } 267 | Log("Got Playlist: " + playlistData.attributes.name); 268 | if (_this.makeFolder) { 269 | _this.dir = _this.directory + "/" + playlistData.attributes.name.replace(/\//g, '-') + '/'; 270 | } 271 | _this.Tracks = lodash.map(playlistData.contents.items, function (item) { 272 | return item.uri; 273 | }); 274 | return typeof cb === "function" ? cb() : void 0; 275 | }; 276 | })(this)); 277 | }; 278 | 279 | Downloader.prototype.processTracks = function (cb) { 280 | Log("Processing " + this.Tracks.length + " Tracks"); 281 | return async.mapSeries(this.Tracks, this.processTrack, cb); 282 | }; 283 | 284 | Downloader.prototype.processTrack = function (track, cb) { 285 | var TempInstance; 286 | TempInstance = new Track(track, this.Spotify, this.dir, this.makeFolder, cb); 287 | return TempInstance; 288 | }; 289 | 290 | return Downloader; 291 | 292 | })(EventEmitter); 293 | 294 | Playlist = (function (superClass) { 295 | extend(Playlist, superClass); 296 | 297 | function Playlist() { 298 | this.addTrackToPlaylist = bind(this.addTrackToPlaylist, this); 299 | this.directory = null; 300 | this.name = null; 301 | this.playlistFile = null; 302 | } 303 | 304 | Playlist.prototype.addTrackToPlaylist = function () { }; 305 | 306 | return Playlist; 307 | 308 | })(EventEmitter); 309 | 310 | module.exports = Downloader; 311 | 312 | }).call(this); 313 | 314 | //# sourceMappingURL=downloader.js.map 315 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | (function() { 3 | var Colors, DIRECTORY, DL, Downloader, FOLDER, GENERATE, PASSWORD, PLAYLIST, Program, USERNAME, getUserHome; 4 | 5 | require('coffee-script'); 6 | 7 | Colors = require('colors'); 8 | 9 | Program = require('commander'); 10 | 11 | Downloader = require('./lib/downloader'); 12 | 13 | getUserHome = (function(_this) { 14 | return function() { 15 | if (process.platform === 'win32') { 16 | return process.env['USERPROFILE']; 17 | } 18 | return process.env['HOME']; 19 | }; 20 | })(this); 21 | 22 | Program.version('0.0.2').option('-u, --username [username]', 'Spotify Playlist Username (required)', null).option('-p, --playlist [playlist]', 'Spotify Playlist (required)', null).option('-d, --directory [directory]', "Directory you want to save the mp3s to, default: " + (getUserHome()) + "/Music", (getUserHome()) + "/Music").option('-f, --folder', "create folder for playlist", null).option('-g, --generate', "generate file for playlist", null).parse(process.argv); 23 | 24 | USERNAME = "USERNAME"; //Program.username; 25 | 26 | PASSWORD = "PASSWORD"; //Program.password; 27 | 28 | PLAYLIST_USER = Program.username; 29 | PLAYLIST_LIST = Program.playlist; 30 | 31 | //PLAYLIST = Program.playlist; 32 | 33 | DIRECTORY = Program.directory; 34 | 35 | FOLDER = Program.folder; 36 | 37 | GENERATE = Program.generate; 38 | 39 | if ((PLAYLIST_USER == null) || (PLAYLIST_LIST == null)) { 40 | console.log('!!! MUST SPECIFY USERNAME & PLAYLIST !!!'.red); 41 | return Program.outputHelp(); 42 | } 43 | 44 | DL = new Downloader(USERNAME, PASSWORD, "spotify:user:" + PLAYLIST_USER + ":playlist:" + PLAYLIST_LIST, DIRECTORY); 45 | 46 | if (FOLDER) { 47 | DL.makeFolder = true; 48 | } 49 | if (GENERATE) { 50 | DL.generatePlaylist = 1; 51 | } 52 | 53 | DL.run(); 54 | 55 | }).call(this); 56 | 57 | //# sourceMappingURL=main.js.map 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-playlist-downloader", 3 | "version": "0.1.4", 4 | "description": "Download spotify playlists (from spotify)", 5 | "main": "main.js", 6 | "dependencies": { 7 | "async": "^0.9.0", 8 | "coffee-script": "^1.9.0", 9 | "colors": "*", 10 | "commander": "*", 11 | "node-id3": "^0.0.4", 12 | "httpreq": "^0.4.16", 13 | "lodash": "^3.0.0", 14 | "mkdirp": "^0.5.0", 15 | "spotify-web": "file:spot_node.tar.gz" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/Lordmau5/spotify-playlist-downloader" 20 | }, 21 | "preferGlobal": false, 22 | "bin": { 23 | "spotify-playlist-downloader": "main.js", 24 | "spd": "main.js" 25 | }, 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /spot_node.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dekiller82/spotify-playlist-downloader-with-windows-gui/0da540b0ef00f8c484718ba4184a0de8088ca617/spot_node.tar.gz --------------------------------------------------------------------------------