├── .babelrc ├── .gitignore ├── LICENSE.md ├── README.md ├── lib ├── index.js └── index.js.map ├── package.json └── src └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015", "stage-0"] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jesse Sessler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DJ Lazy 2 | --- 3 | DJ Lazy is a command-line tool to remove the burden of staying up to date with the latest music. 4 | 5 | [Allmusic.com](Allmusic.com) puts out a fresh list of new releases once a week. Once that list is up on their website you can run dj-lazy in your command line to add all available tracks on Spotify to a new Spotify playlist. 6 | 7 | # Installation 8 | --- 9 | ```bash 10 | npm install -g dj-lazy 11 | ``` 12 | 13 | # Prerequisites 14 | --- 15 | To use DJ Lazy you first need a clientId and clientSecret token so DJ Lazy can use the Spotify Web API. 16 | 17 | 1. Go to [Spotify's Developer Portal](https://developer.spotify.com/), login, and go to **My Apps** 18 | 2. Follow the steps to create a new app. In the **Redirect URIs** field add "http://localhost:8085/spotify-auth" and click **Save** (this allows DJ lazy to retrieve your authentication token) 19 | 3. Store your clientId and clientSecret as ENV variables as: 20 | ```bash 21 | DJ_LAZY_CLIENT_ID= 22 | DJ_LAZY_CLIENT_SECRET= 23 | ``` 24 | 25 | # Usage 26 | --- 27 | ```bash 28 | dj-lazy 29 | ``` 30 | *Note:* DJ Lazy requires an authentication token from Spotify on every run to make changes to your account. Therefore DJ Lazy will open a browser window to authenticate you. If you are already authenticated the window will open and subsequently close. 31 | 32 | # Options 33 | --- 34 | ```bash 35 | -m, --max : Max number of albums to add (default: none) 36 | -s, --status : Playlist status, either public or private (default: private) 37 | ``` 38 | # TODO 39 | --- 40 | * Get music from Allmusic's genre pages 41 | * Add more music sources 42 | * Don't get new auth token every time 43 | * Prevent duplicate playlists 44 | * Progress bar instead of logging 45 | 46 | # License 47 | --- 48 | MIT -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var _open = require('open'); 5 | 6 | var _open2 = _interopRequireDefault(_open); 7 | 8 | var _xRay = require('x-ray'); 9 | 10 | var _xRay2 = _interopRequireDefault(_xRay); 11 | 12 | var _spotifyWebApiNode = require('spotify-web-api-node'); 13 | 14 | var _spotifyWebApiNode2 = _interopRequireDefault(_spotifyWebApiNode); 15 | 16 | var _express = require('express'); 17 | 18 | var _express2 = _interopRequireDefault(_express); 19 | 20 | var _q = require('q'); 21 | 22 | var _q2 = _interopRequireDefault(_q); 23 | 24 | var _commander = require('commander'); 25 | 26 | var _commander2 = _interopRequireDefault(_commander); 27 | 28 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 29 | 30 | // Get command line args 31 | _commander2.default.option('-m, --max', 'The max number of albums to add').option('-s, --status', 'The playlist status (public, private)').parse(process.argv); 32 | 33 | var store = { 34 | userId: '', 35 | playlistId: '', 36 | scrapedAlbums: [], 37 | albumsSuccess: [] 38 | }; 39 | 40 | var config = { 41 | scopes: ['playlist-modify-public', 'playlist-modify-private'], 42 | redirectPath: '/spotify-auth', 43 | port: 8085 44 | }; 45 | 46 | var clientId = process.env.DJ_LAZY_CLIENT_ID; 47 | var clientSecret = process.env.DJ_LAZY_CLIENT_SECRET; 48 | var redirectUri = 'http://localhost:' + config.port + config.redirectPath; 49 | 50 | if (!clientId || !clientSecret) { 51 | var err = new Error('Missing cliendId or clientSecret\nEnsure you have DJ_LAZY_CLIENT_ID and DJ_LAZY_CLIENT_SECRET set as ENV variables'); 52 | throw err; 53 | } 54 | 55 | // Create Spotify API wrapper 56 | var spotifyApi = new _spotifyWebApiNode2.default({ clientId: clientId, clientSecret: clientSecret, redirectUri: redirectUri }); 57 | 58 | // Create the authorization URL 59 | var authorizeURL = spotifyApi.createAuthorizeURL(config.scopes, 'mystate'); 60 | 61 | var app = (0, _express2.default)(); 62 | var server = undefined; 63 | 64 | app.get(config.redirectPath, function (req, res) { 65 | var authToken = req.query.code; 66 | res.send(''); 67 | server.close(); 68 | startScraping(authToken); 69 | }); 70 | 71 | server = app.listen(config.port, function (_) { 72 | return console.log('Server listening on port ' + config.port + '!\n'); 73 | }); 74 | (0, _open2.default)(authorizeURL); 75 | 76 | var authorize = function authorize(authToken) { 77 | return spotifyApi.authorizationCodeGrant(authToken); 78 | }; 79 | 80 | var setAccessToken = function setAccessToken(data) { 81 | return spotifyApi.setAccessToken(data.body['access_token']); 82 | }; 83 | 84 | var fetchUser = function fetchUser(_) { 85 | return spotifyApi.getMe(); 86 | }; 87 | 88 | var createPlaylist = function createPlaylist(_) { 89 | var date = new Date(); 90 | var title = 'DJ Lazy ' + date.toLocaleDateString(); 91 | var status = 'public'; 92 | if (_commander2.default.status === 'private' || _commander2.default.status === 'public') { 93 | status = _commander2.default.status; 94 | } 95 | return spotifyApi.createPlaylist(store.userId, title, { public: status == 'public' }); 96 | }; 97 | 98 | var scrapeForAlbums = function scrapeForAlbums() { 99 | var deferred = _q2.default.defer(); 100 | var x = (0, _xRay2.default)(); 101 | x('http://www.allmusic.com/newreleases', x('.featured-rows .row .featured', [{ 102 | artist: '.artist a:first-child', 103 | title: '.title a:first-child' 104 | }]))(function (err, data) { 105 | if (err) { 106 | console.error('Error trying to scrape'); 107 | throw err; 108 | } else { 109 | deferred.resolve(data); 110 | } 111 | }); 112 | return deferred.promise; 113 | }; 114 | 115 | var getAlbumsTracks = function getAlbumsTracks(albumIds) { 116 | return _q2.default.all(albumIds.map(function (id) { 117 | return spotifyApi.getAlbumTracks(id); 118 | })); 119 | }; 120 | 121 | function delay(delay) { 122 | var q = _q2.default.defer(); 123 | setTimeout(q.resolve.bind(q), delay); 124 | return q.promise; 125 | } 126 | 127 | var addTracksToPlaylist = function addTracksToPlaylist(trackUris) { 128 | var defer = _q2.default.defer(); 129 | var promise = defer.promise; 130 | for (var i = 0; i < trackUris.length; i += 50) { 131 | (function (i) { 132 | var endIndex = trackUris.length <= i + 50 ? trackUris.length : i + 50; 133 | promise = promise.then(function (_) { 134 | return spotifyApi.addTracksToPlaylist(store.userId, store.playlistId, trackUris.slice(i, endIndex)); 135 | }); 136 | promise = promise.then(function (_) { 137 | return delay(2000); 138 | }); 139 | })(i); 140 | } 141 | defer.resolve(); 142 | return promise; 143 | }; 144 | 145 | function startScraping(authToken) { 146 | 147 | authorize(authToken).then(setAccessToken).then(fetchUser).then(function (userData) { 148 | store.userId = userData.body.id; 149 | console.log('Creating playlist...'); 150 | return createPlaylist(); 151 | }).then(function (playlistData) { 152 | store.playlistId = playlistData.body.id; 153 | console.log('Fetching albums from Allmusic...'); 154 | return scrapeForAlbums(); 155 | }).then(function (scrapedAlbums) { 156 | store.scrapedAlbums = scrapedAlbums; 157 | console.log('Searching Spotify...'); 158 | return _q2.default.all(scrapedAlbums.map(function (album) { 159 | return spotifyApi.searchAlbums('album:' + album.title + ' artist:' + album.artist); 160 | })); 161 | }).then(function (spotifyAlbumResultsData) { 162 | console.log('Fetching tracks...'); 163 | var maxAlbums = _commander2.default.max && parseInt(_commander2.default.max); 164 | var topMatchIds = spotifyAlbumResultsData.map(function (result, i) { 165 | if (result.body.albums && result.body.albums.items && result.body.albums.items.length) { 166 | if (!maxAlbums || store.albumsSuccess.length < maxAlbums) { 167 | store.albumsSuccess.push(store.scrapedAlbums[i]); 168 | return result.body.albums.items[0].id; 169 | } 170 | } 171 | }).filter(function (id) { 172 | return !!id; 173 | }); 174 | return getAlbumsTracks(topMatchIds); 175 | }).then(function (albumsTracksData) { 176 | console.log('Adding tracks to Spotify...'); 177 | var albumsTracksUris = albumsTracksData.map(function (result) { 178 | return result.body.items.map(function (item) { 179 | return item.uri; 180 | }); 181 | }); 182 | var flatAlbumsTracksUris = [].concat.apply([], albumsTracksUris); 183 | return addTracksToPlaylist(flatAlbumsTracksUris); 184 | }).then(function (data) { 185 | console.log('Finished!'); 186 | console.log('Found Spotify Albums For'); 187 | console.log('========================='); 188 | store.albumsSuccess.map(function (a) { 189 | return console.log(a.title + ' by ' + a.artist); 190 | }); 191 | process.exit(); 192 | }).catch(function (err) { 193 | console.log('Something went wrong!', err); 194 | throw err; 195 | }); 196 | } 197 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/index.js"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["\n\nimport open from 'open';\nimport Xray from 'x-ray';\nimport SpotifyWebApi from 'spotify-web-api-node';\nimport express from 'express';\nimport Q from 'q';\nimport cliOptions from 'commander';\n\n// Get command line args\ncliOptions\n.option('-m, --max', 'The max number of albums to add')\n.option('-s, --status', 'The playlist status (public, private)')\n.parse(process.argv);\n\nconst store = {\n\tuserId: '',\n\tplaylistId: '',\n\tscrapedAlbums: [],\n\talbumsSuccess: []\n};\n\nconst config = {\n\tscopes : ['playlist-modify-public', 'playlist-modify-private'],\n\tredirectPath : '/spotify-auth',\n\tport : 8085\n};\n\nconst clientId = process.env.DJ_LAZY_CLIENT_ID;\nconst clientSecret = process.env.DJ_LAZY_CLIENT_SECRET;\nconst redirectUri = `http://localhost:${config.port}${config.redirectPath}`;\n\nif (!clientId || !clientSecret) {\n\tconst err = new Error('Missing cliendId or clientSecret\\nEnsure you have DJ_LAZY_CLIENT_ID and DJ_LAZY_CLIENT_SECRET set as ENV variables');\n\tthrow err;\n}\n\n// Create Spotify API wrapper\nconst spotifyApi = new SpotifyWebApi({clientId, clientSecret, redirectUri});\n\n// Create the authorization URL\nconst authorizeURL = spotifyApi.createAuthorizeURL(config.scopes, 'mystate');\n\nconst app = express();\nlet server;\n\napp.get(config.redirectPath, (req, res) => {\n\tconst authToken = req.query.code;\n\tres.send('');\n\tserver.close();\n\tstartScraping(authToken);\n});\n\nserver = app.listen(config.port, _ => console.log(`Server listening on port ${config.port}!\\n`) );\nopen(authorizeURL);\n\nconst authorize = (authToken) => {\n\treturn spotifyApi.authorizationCodeGrant(authToken);\n}\n\nconst setAccessToken = (data) => {\n\treturn spotifyApi.setAccessToken(data.body['access_token']);\n}\n\nconst fetchUser = _ => {\n\treturn spotifyApi.getMe();\n}\n\nconst createPlaylist = _ => {\n\tconst date = new Date();\n\tconst title = `DJ Lazy ${date.toLocaleDateString()}`;\n\tlet status = 'public'; \n\tif (cliOptions.status === 'private' || cliOptions.status === 'public') {\n\t\tstatus = cliOptions.status;\n\t}\n\treturn spotifyApi.createPlaylist(store.userId, title, {public: status == 'public'});\n}\n\nconst scrapeForAlbums = () => {\n\tvar deferred = Q.defer();\n\tconst x = Xray();\n\tx('http://www.allmusic.com/newreleases', \n\t\tx('.featured-rows .row .featured', [{\n\t\t\tartist: '.artist a:first-child',\n\t\t\ttitle: '.title a:first-child'\n\t\t}])\n\t)((err, data) => {\n\t\tif (err) {\n\t\t\tconsole.error('Error trying to scrape');\n\t\t\tthrow err;\n\t\t} else {\n\t\t\tdeferred.resolve(data);\n\t\t}\n\t});\n\treturn deferred.promise;\n}\n\nconst getAlbumsTracks = (albumIds) => {\n\treturn Q.all(albumIds.map(id => spotifyApi.getAlbumTracks(id) ));\n}\n\nfunction delay (delay) {\n\tvar q = Q.defer();\n\tsetTimeout(q.resolve.bind(q), delay);\n\treturn q.promise;\n}\n\nconst addTracksToPlaylist = (trackUris) => {\n\tlet defer = Q.defer();\n\tlet promise = defer.promise;\n\tfor (var i = 0; i < trackUris.length; i += 50) {\n\t\t(i => {\n\t\t\tconst endIndex = trackUris.length <= i + 50 ? trackUris.length : i + 50;\n\t\t\tpromise = promise.then(_ => spotifyApi.addTracksToPlaylist(store.userId, store.playlistId, trackUris.slice(i, endIndex)));\n\t\t\tpromise = promise.then(_ => delay(2000));\n\t\t})(i);\n\t}\n\tdefer.resolve();\n\treturn promise;\n}\n\nfunction startScraping(authToken) {\n\n\tauthorize(authToken)\n\t.then(setAccessToken)\n\t.then(fetchUser)\n\t.then(userData => {\n\t\tstore.userId = userData.body.id;\n\t\tconsole.log('Creating playlist...');\n\t\treturn createPlaylist();\n\t})\n\t.then(playlistData => {\n\t\tstore.playlistId = playlistData.body.id;\n\t\tconsole.log('Fetching albums from Allmusic...');\n\t\treturn scrapeForAlbums();\n\t})\n\t.then(scrapedAlbums => {\n\t\tstore.scrapedAlbums = scrapedAlbums;\n\t\tconsole.log('Searching Spotify...');\n\t\treturn Q.all(scrapedAlbums.map(album => {\n\t\t\treturn spotifyApi.searchAlbums(`album:${album.title} artist:${album.artist}`);\n\t\t}));\n\t})\n\t.then(spotifyAlbumResultsData => {\n\t\tconsole.log('Fetching tracks...');\n\t\tconst maxAlbums = cliOptions.max && parseInt(cliOptions.max);\n\t\tconst topMatchIds = spotifyAlbumResultsData.map((result, i) => {\n\t\t\tif (result.body.albums && result.body.albums.items && result.body.albums.items.length) {\n\t\t\t\tif (!maxAlbums || store.albumsSuccess.length < maxAlbums) {\n\t\t\t\t\tstore.albumsSuccess.push(store.scrapedAlbums[i]);\n\t\t\t\t\treturn result.body.albums.items[0].id;\n\t\t\t\t}\n\t\t\t}\n\t\t}).filter(id => !!id);\n\t\treturn getAlbumsTracks(topMatchIds);\n\t})\n\t.then(albumsTracksData => {\n\t\tconsole.log('Adding tracks to Spotify...');\n\t\tconst albumsTracksUris = albumsTracksData.map(result => {\n\t\t\treturn result.body.items.map(item => item.uri);\n\t\t});\n\t\tconst flatAlbumsTracksUris = [].concat.apply([], albumsTracksUris);\n\t\treturn addTracksToPlaylist(flatAlbumsTracksUris);\n\t})\n\t.then(data => {\n\t\tconsole.log('Finished!');\n\t\tconsole.log('Found Spotify Albums For');\n\t\tconsole.log('=========================');\n\t\tstore.albumsSuccess.map(a => console.log(`${a.title} by ${a.artist}`));\n\t\tprocess.exit();\n\t})\n\t.catch(function(err) {\n\t\tconsole.log('Something went wrong!', err);\n\t\tthrow err;\n\t});\n}"]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dj-lazy", 3 | "version": "0.0.3", 4 | "description": "Create Spotify playlists based on Allmusic.com new releases", 5 | "bin": { 6 | "dj-lazy": "./lib/index.js" 7 | }, 8 | "main": "lib/index.js", 9 | "scripts": { 10 | "watch": "babel src --watch --out-dir lib --source-maps" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/jemise111/dj-lazy" 15 | }, 16 | "author": "Jesse Sessler ", 17 | "license": "MIT", 18 | "keyword": [ 19 | "music", 20 | "allmusic", 21 | "lazy", 22 | "dj", 23 | "playlist", 24 | "spotify" 25 | ], 26 | "devDependencies": { 27 | "babel-cli": "^6.5.1", 28 | "babel-core": "^6.5.2", 29 | "babel-plugin-transform-async-to-generator": "^6.5.0", 30 | "babel-polyfill": "^6.5.0", 31 | "babel-preset-es2015": "^6.5.0", 32 | "babel-preset-stage-0": "^6.5.0" 33 | }, 34 | "dependencies": { 35 | "commander": "^2.9.0", 36 | "express": "^4.13.4", 37 | "open": "0.0.5", 38 | "q": "^1.4.1", 39 | "spotify-web-api-node": "^2.2.0", 40 | "x-ray": "^2.0.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import open from 'open'; 4 | import Xray from 'x-ray'; 5 | import SpotifyWebApi from 'spotify-web-api-node'; 6 | import express from 'express'; 7 | import Q from 'q'; 8 | import cliOptions from 'commander'; 9 | 10 | // Get command line args 11 | cliOptions 12 | .option('-m, --max', 'The max number of albums to add') 13 | .option('-s, --status', 'The playlist status (public, private)') 14 | .parse(process.argv); 15 | 16 | const store = { 17 | userId: '', 18 | playlistId: '', 19 | scrapedAlbums: [], 20 | albumsSuccess: [] 21 | }; 22 | 23 | const config = { 24 | scopes : ['playlist-modify-public', 'playlist-modify-private'], 25 | redirectPath : '/spotify-auth', 26 | port : 8085 27 | }; 28 | 29 | const clientId = process.env.DJ_LAZY_CLIENT_ID; 30 | const clientSecret = process.env.DJ_LAZY_CLIENT_SECRET; 31 | const redirectUri = `http://localhost:${config.port}${config.redirectPath}`; 32 | 33 | if (!clientId || !clientSecret) { 34 | const err = new Error('Missing cliendId or clientSecret\nEnsure you have DJ_LAZY_CLIENT_ID and DJ_LAZY_CLIENT_SECRET set as ENV variables'); 35 | throw err; 36 | } 37 | 38 | // Create Spotify API wrapper 39 | const spotifyApi = new SpotifyWebApi({clientId, clientSecret, redirectUri}); 40 | 41 | // Create the authorization URL 42 | const authorizeURL = spotifyApi.createAuthorizeURL(config.scopes, 'mystate'); 43 | 44 | const app = express(); 45 | let server; 46 | 47 | app.get(config.redirectPath, (req, res) => { 48 | const authToken = req.query.code; 49 | res.send(''); 50 | server.close(); 51 | startScraping(authToken); 52 | }); 53 | 54 | server = app.listen(config.port, _ => console.log(`Server listening on port ${config.port}!\n`) ); 55 | open(authorizeURL); 56 | 57 | const authorize = (authToken) => { 58 | return spotifyApi.authorizationCodeGrant(authToken); 59 | } 60 | 61 | const setAccessToken = (data) => { 62 | return spotifyApi.setAccessToken(data.body['access_token']); 63 | } 64 | 65 | const fetchUser = _ => { 66 | return spotifyApi.getMe(); 67 | } 68 | 69 | const createPlaylist = _ => { 70 | const date = new Date(); 71 | const title = `DJ Lazy ${date.toLocaleDateString()}`; 72 | let status = 'public'; 73 | if (cliOptions.status === 'private' || cliOptions.status === 'public') { 74 | status = cliOptions.status; 75 | } 76 | return spotifyApi.createPlaylist(store.userId, title, {public: status == 'public'}); 77 | } 78 | 79 | const scrapeForAlbums = () => { 80 | var deferred = Q.defer(); 81 | const x = Xray(); 82 | x('http://www.allmusic.com/newreleases', 83 | x('.featured-rows .row .featured', [{ 84 | artist: '.artist a:first-child', 85 | title: '.title a:first-child' 86 | }]) 87 | )((err, data) => { 88 | if (err) { 89 | console.error('Error trying to scrape'); 90 | throw err; 91 | } else { 92 | deferred.resolve(data); 93 | } 94 | }); 95 | return deferred.promise; 96 | } 97 | 98 | const getAlbumsTracks = (albumIds) => { 99 | return Q.all(albumIds.map(id => spotifyApi.getAlbumTracks(id) )); 100 | } 101 | 102 | function delay (delay) { 103 | var q = Q.defer(); 104 | setTimeout(q.resolve.bind(q), delay); 105 | return q.promise; 106 | } 107 | 108 | const addTracksToPlaylist = (trackUris) => { 109 | let defer = Q.defer(); 110 | let promise = defer.promise; 111 | for (var i = 0; i < trackUris.length; i += 50) { 112 | (i => { 113 | const endIndex = trackUris.length <= i + 50 ? trackUris.length : i + 50; 114 | promise = promise.then(_ => spotifyApi.addTracksToPlaylist(store.userId, store.playlistId, trackUris.slice(i, endIndex))); 115 | promise = promise.then(_ => delay(2000)); 116 | })(i); 117 | } 118 | defer.resolve(); 119 | return promise; 120 | } 121 | 122 | function startScraping(authToken) { 123 | 124 | authorize(authToken) 125 | .then(setAccessToken) 126 | .then(fetchUser) 127 | .then(userData => { 128 | store.userId = userData.body.id; 129 | console.log('Creating playlist...'); 130 | return createPlaylist(); 131 | }) 132 | .then(playlistData => { 133 | store.playlistId = playlistData.body.id; 134 | console.log('Fetching albums from Allmusic...'); 135 | return scrapeForAlbums(); 136 | }) 137 | .then(scrapedAlbums => { 138 | store.scrapedAlbums = scrapedAlbums; 139 | console.log('Searching Spotify...'); 140 | return Q.all(scrapedAlbums.map(album => { 141 | return spotifyApi.searchAlbums(`album:${album.title} artist:${album.artist}`); 142 | })); 143 | }) 144 | .then(spotifyAlbumResultsData => { 145 | console.log('Fetching tracks...'); 146 | const maxAlbums = cliOptions.max && parseInt(cliOptions.max); 147 | const topMatchIds = spotifyAlbumResultsData.map((result, i) => { 148 | if (result.body.albums && result.body.albums.items && result.body.albums.items.length) { 149 | if (!maxAlbums || store.albumsSuccess.length < maxAlbums) { 150 | store.albumsSuccess.push(store.scrapedAlbums[i]); 151 | return result.body.albums.items[0].id; 152 | } 153 | } 154 | }).filter(id => !!id); 155 | return getAlbumsTracks(topMatchIds); 156 | }) 157 | .then(albumsTracksData => { 158 | console.log('Adding tracks to Spotify...'); 159 | const albumsTracksUris = albumsTracksData.map(result => { 160 | return result.body.items.map(item => item.uri); 161 | }); 162 | const flatAlbumsTracksUris = [].concat.apply([], albumsTracksUris); 163 | return addTracksToPlaylist(flatAlbumsTracksUris); 164 | }) 165 | .then(data => { 166 | console.log('Finished!'); 167 | console.log('Found Spotify Albums For'); 168 | console.log('========================='); 169 | store.albumsSuccess.map(a => console.log(`${a.title} by ${a.artist}`)); 170 | process.exit(); 171 | }) 172 | .catch(function(err) { 173 | console.log('Something went wrong!', err); 174 | throw err; 175 | }); 176 | } --------------------------------------------------------------------------------