diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 20aa5db..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,19 +0,0 @@ -image: node:6-slim -services: - - mongo - -before_script: - - npm install - -test: - stage: test - script: - - npm test - tags: - - docker - -cache: - key: "$CI_BUILD_REF_NAME" - untracked: true - paths: - - node_modules diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..63cca50 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "2" + +services: + app: + build: ./ + environment: + DEBUG: "match.audio*" + VUE_ENV: server + DATABASE_URL: + GOOGLE_EMAIL: + GOOGLE_PASSWORD: + XBOX_CLIENT_ID: + XBOX_CLIENT_SECRET: + YOUTUBE_KEY: + SPOTIFY_CLIENT_ID: + SPOTIFY_CLIENT_SECRET: + volumes: + - ./:/app:cached + ports: + - "3000:3000" + command: yarn run watch-server + database: + image: "postgres:9.6" + ports: + - "5432:5432" + environment: + POSTGRES_PASSWORD: "password" + POSTGRES_USER: "matchaudio" + POSTGRES_DB: "matchaudio" + diff --git a/lib/lookup.js b/lib/lookup.js index d6f226c..fc4361b 100644 --- a/lib/lookup.js +++ b/lib/lookup.js @@ -13,7 +13,8 @@ fs.readdirSync(path.join(__dirname, 'services')).forEach(function(file) { export default function* (url) { let matchedService; for (let service of services) { - matchedService = yield service.match(url); + console.log(service) + matchedService = service.match(url); if (matchedService) { const result = yield service.parseUrl(url); return yield service.lookupId(result.id, result.type); diff --git a/lib/services/deezer/index.js b/lib/services/deezer/index.js index a2dea0a..2c058ac 100644 --- a/lib/services/deezer/index.js +++ b/lib/services/deezer/index.js @@ -1,86 +1,110 @@ import { parse } from 'url'; import request from 'superagent'; import 'superagent-bluebird-promise'; -import { match as urlMatch } from './url'; - -export let id = 'deezer'; +import urlMatch from './url'; const apiRoot = 'https://api.deezer.com'; -export const match = urlMatch; - export function parseUrl(url) { - let matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/); + const matches = parse(url).path.match(/\/(album|track)[/]+([^/]+)/); if (matches && matches[2]) { return module.exports.lookupId(matches[2], matches[1]); - } else { - throw new Error(); } -}; + throw new Error(); +} + +function exactMatch(needle, haystack, type, various) { + // try to find exact match + return haystack.find((entry) => { + if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) { + return false; + } + const title = entry[type].title; + if (title) { + return entry; + } + return false; + }); +} + +function looseMatch(needle, haystack, type, various) { + // try to find exact match + return haystack.find((entry) => { + if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) { + return false; + } + const name = entry[type].title || entry[type].name; + if (name.indexOf(needle) >= 0) { + return entry[type]; + } + return false; + }); +} + export function* lookupId(id, type) { - let path = '/' + type + '/' + id; + const path = `/${type}/${id}`; - let {body} = yield request.get(apiRoot + path).promise(); + const { body } = yield request.get(apiRoot + path).promise(); if (!body || body.error) { - let error = new Error('Not Found'); + const error = new Error('Not Found'); error.status = 404; return Promise.reject(error); } - let item = body; - let coverUrl = item.cover || item.album.cover; + const item = body; + const coverUrl = item.cover || item.album.cover; let cover = 'test'; // nasty hacks for superagent-bluebird-promise try { cover = yield request.get(coverUrl).redirects(0); - } catch(err) { + } catch (err) { cover = err.originalError.response; } - let artwork = { + const artwork = { small: cover.headers.location.replace('120x120', '200x200'), - large: cover.headers.location.replace('120x120', '800x800') + large: cover.headers.location.replace('120x120', '800x800'), }; if (type === 'album') { return Promise.resolve({ service: 'deezer', - type: type, + type, id: item.id, name: item.title, streamUrl: item.link, purchaseUrl: null, - artwork: artwork, + artwork, artist: { - name: item.artist.name - } + name: item.artist.name, + }, }); } else if (type === 'track') { return Promise.resolve({ service: 'deezer', - type: type, + type, id: item.id, name: item.title, streamUrl: item.album.link, purchaseUrl: null, - artwork: artwork, + artwork, artist: { - name: item.artist.name + name: item.artist.name, }, album: { - name: item.album.title - } + name: item.album.title, + }, }); - } else { - return Promise.reject(new Error()); } -}; + return Promise.reject(new Error()); +} -export function* search(data, original={}) { - let cleanParam = function(str) { - return str.replace(/[\:\?\&]+/, ''); - }; - let query, album; - let {type} = data; +export function* search(data, original = {}) { + function cleanParam(str) { + return str.replace(/[:?&]+/, ''); + } + let query; + let album; + const { type } = data; const various = data.artist.name === 'Various Artists' || data.artist.name === 'Various'; @@ -89,65 +113,40 @@ export function* search(data, original={}) { if (various) { query = cleanParam(data.name); } else { - query = cleanParam(data.artist.name) + ' ' + cleanParam(data.name); + query = `${cleanParam(data.artist.name)} ${cleanParam(data.name)}`; } album = data.name; } else if (type === 'track') { - query = cleanParam(data.artist.name) + ' ' + cleanParam(data.albumName) + ' ' + cleanParam(data.name); + query = `${cleanParam(data.artist.name)} ${cleanParam(data.albumName)} ${cleanParam(data.name)}`; album = data.albumName; } - var path = '/search/' + type + '?q=' + encodeURIComponent(query); + const path = `/search/${type}?q=${encodeURIComponent(query)}`; - let response = yield request.get(apiRoot + path); + const response = yield request.get(apiRoot + path); const name = original.name || data.name; if (response.body.data.length > 0) { - let match; - if (!(match = exactMatch(name, response.body.data, data.type, various))) { + let match = exactMatch(name, response.body.data, data.type, various); + if (!match) { match = looseMatch(name, response.body.data, data.type, various); } return yield module.exports.lookupId(response.body.data[0].id, type); - } else { - var matches = album.match(/^[^\(\[]+/); - if (matches && matches[0] && matches[0] !== album) { - var cleanedData = JSON.parse(JSON.stringify(data)); - if (type === 'album') { - cleanedData.name = matches[0].trim(); - } else if (type === 'track') { - cleanedData.albumName = matches[0].trim(); - } - return yield module.exports.search(cleanedData, data); - } else { - return Promise.resolve({service: 'deezer'}); - } } -}; - -function exactMatch(needle, haystack, type, various) { - // try to find exact match - return haystack.find(function(entry) { - if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) { - return false; + const matches = album.match(/^[^([]+/); + if (matches && matches[0] && matches[0] !== album) { + const cleanedData = JSON.parse(JSON.stringify(data)); + if (type === 'album') { + cleanedData.name = matches[0].trim(); + } else if (type === 'track') { + cleanedData.albumName = matches[0].trim(); } - entry = entry[type]; - if (entry.title === needle) { - return entry; - } - }); + return yield module.exports.search(cleanedData, data); + } + return Promise.resolve({ service: 'deezer' }); } -function looseMatch(needle, haystack, type, various) { - // try to find exact match - return haystack.find(function(entry) { - if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) { - return false; - } - const name = entry[type].title || entry[type].name; - if (name.indexOf(needle) >= 0) { - return entry[type]; - } - }); -} +export const id = 'deezer'; +export const match = urlMatch; diff --git a/lib/services/deezer/url.js b/lib/services/deezer/url.js index f479ac6..2431823 100644 --- a/lib/services/deezer/url.js +++ b/lib/services/deezer/url.js @@ -1,10 +1,10 @@ import { parse } from 'url'; -export function* match(url) { +export default function match(url) { const parsed = parse(url); if (!parsed.host.match(/deezer\.com$/)) { return false; } - const matches = parsed.path.match(/\/(album|track)[\/]+([^\/]+)/); + const matches = parsed.path.match(/\/(album|track)[/]+([^/]+)/); return matches.length > 1; -}; +} diff --git a/lib/services/google/index.js b/lib/services/google/index.js index 3832aab..982b6ec 100644 --- a/lib/services/google/index.js +++ b/lib/services/google/index.js @@ -1,21 +1,143 @@ import { parse } from 'url'; import bluebird from 'bluebird'; import PlayMusic from 'playmusic'; -import { match as urlMatch } from './url'; +import debuglog from 'debug'; +import urlMatch from './url'; + +const debug = debuglog('match.audio'); const pm = bluebird.promisifyAll(new PlayMusic()); -export let id = 'google'; - if (!process.env.GOOGLE_EMAIL || !process.env.GOOGLE_PASSWORD) { - console.warn('GOOGLE_EMAIL or GOOGLE_PASSWORD environment variables not found, deactivating Google Play Music.'); + debug('GOOGLE_EMAIL or GOOGLE_PASSWORD environment variables not found, deactivating Google Play Music.'); } -let ready = pm.initAsync({email: process.env.GOOGLE_EMAIL, password: process.env.GOOGLE_PASSWORD}).catch(function(err) { - console.log(err); -}); +const ready = pm.initAsync({ + email: process.env.GOOGLE_EMAIL, + password: process.env.GOOGLE_PASSWORD }) + .catch((err) => { + debug(err); + }); -export const match = urlMatch; +export function* lookupId(id, type) { + yield ready; + if (type === 'album') { + const album = yield pm.getAlbumAsync(id, false); + return { + service: 'google', + type: 'album', + id: album.albumId, + name: album.name, + streamUrl: `https://play.google.com/music/m/${album.albumId}?signup_if_needed=1`, + purchaseUrl: `https://play.google.com/store/music/album?id=${album.albumId}`, + artwork: { + small: album.albumArtRef.replace('http:', 'https:'), + large: album.albumArtRef.replace('http:', 'https:'), + }, + artist: { + name: album.artist, + }, + }; + } else if (type === 'track') { + const track = yield pm.getAllAccessTrackAsync(id); + return { + service: 'google', + type: 'track', + id: track.nid, + name: track.title, + streamUrl: `https://play.google.com/music/m/${track.nid}?signup_if_needed=1`, + purchaseUrl: `https://play.google.com/store/music/album?id=${track.albumId}`, + artwork: { + small: track.albumArtRef[0].url.replace('http:', 'https:'), + large: track.albumArtRef[0].url.replace('http:', 'https:'), + }, + album: { + name: track.album, + }, + artist: { + name: track.artist, + }, + }; + } + return { service: 'google' }; +} + +function exactMatch(needle, haystack, type) { + // try to find exact match + return haystack.find((entry) => { + if (!entry[type]) { + return false; + } + const title = entry[type].title; + if (title === needle) { + return entry; + } + return false; + }); +} + +function looseMatch(needle, haystack, type) { + // try to find exact match + return haystack.find((entry) => { + if (!entry[type]) { + return false; + } + const name = entry[type].title || entry[type].name; + if (name.indexOf(needle) >= 0) { + return entry[type]; + } + return false; + }); +} + +export function* search(data, original = {}) { + yield ready; + let query; + let album; + const type = data.type; + + if (type === 'album') { + query = `${data.artist.name} ${data.name}`; + album = data.name; + } else if (type === 'track') { + query = `${data.artist.name} ${data.albumName} ${data.name}`; + album = data.albumName; + } + + const result = yield pm.searchAsync(query, 5); + + if (!result.entries) { + const matches = album.match(/^[^([]+/); + if (matches && matches[0]) { + const cleanedData = JSON.parse(JSON.stringify(data)); + if (type === 'album') { + cleanedData.name = data.name.match(/^[^([]+/)[0].trim(); + } else if (type === 'track') { + cleanedData.albumName = data.albumName.match(/^[^([]+/)[0].trim(); + cleanedData.name = data.name.match(/^[^([]+/)[0].trim(); + } + return yield search(cleanedData, data); + } + return { service: 'google' }; + } + + const name = original.name || data.name; + + let match = exactMatch(name, result.entries, data.type); + if (!match) { + match = looseMatch(name, result.entries, data.type); + } + + if (!match) { + return { service: 'google' }; + } + if (type === 'album') { + return yield lookupId(match.album.albumId, type); + } else if (type === 'track') { + return yield lookupId(match.track.storeId, type); + } + return { service: 'google' }; +} export function* parseUrl(url) { yield ready; @@ -34,131 +156,16 @@ export function* parseUrl(url) { } if (id.length > 0) { - return {id: id, type: type}; - } else { - return yield search({type: type, name: album, artist: {name: artist}}); + return { id, type }; } - } else if(path) { + return yield search({ type, name: album, artist: { name: artist } }); + } else if (path) { const matches = path.match(/\/music\/m\/([\w]+)/); const type = matches[1][0] === 'T' ? 'track' : 'album'; return yield lookupId(matches[1], type); } return false; -}; - -export function* lookupId(id, type) { - yield ready; - if (type === 'album') { - const album = yield pm.getAlbumAsync(id, false); - return { - service: 'google', - type: 'album', - id: album.albumId, - name: album.name, - streamUrl: 'https://play.google.com/music/m/' + album.albumId + '?signup_if_needed=1', - purchaseUrl: 'https://play.google.com/store/music/album?id=' + album.albumId, - artwork: { - small: album.albumArtRef.replace('http:', 'https:'), - large: album.albumArtRef.replace('http:', 'https:') - }, - artist: { - name: album.artist - } - }; - } else if (type === 'track') { - const track = yield pm.getAllAccessTrackAsync(id); - return { - service: 'google', - type: 'track', - id: track.nid, - name: track.title, - streamUrl: 'https://play.google.com/music/m/' + track.nid + '?signup_if_needed=1', - purchaseUrl: 'https://play.google.com/store/music/album?id=' + track.albumId, - artwork: { - small: track.albumArtRef[0].url.replace('http:', 'https:'), - large: track.albumArtRef[0].url.replace('http:', 'https:') - }, - album: { - name: track.album - }, - artist: { - name: track.artist - } - }; - } -}; - -export function* search(data, original={}) { - yield ready; - let query, album; - const type = data.type; - - if (type === 'album') { - query = data.artist.name + ' ' + data.name; - album = data.name; - } else if (type === 'track') { - query = data.artist.name + ' ' + data.albumName + ' ' + data.name; - album = data.albumName; - } - - let result = yield pm.searchAsync(query, 5) - - if (!result.entries) { - const matches = album.match(/^[^\(\[]+/); - if (matches && matches[0]) { - const cleanedData = JSON.parse(JSON.stringify(data)); - if (type === 'album') { - cleanedData.name = data.name.match(/^[^\(\[]+/)[0].trim(); - } else if (type === 'track') { - cleanedData.albumName = data.albumName.match(/^[^\(\[]+/)[0].trim(); - cleanedData.name = data.name.match(/^[^\(\[]+/)[0].trim(); - } - return yield search(cleanedData, data); - } else { - return {service: 'google'}; - } - } - - const name = original.name || data.name; - - let match; - if (!(match = exactMatch(name, result.entries, data.type))) { - match = looseMatch(name, result.entries, data.type); - } - - if (!match) { - return {service: 'google'}; - } else { - if (type === 'album') { - return yield lookupId(match.album.albumId, type); - } else if (type === 'track') { - return yield lookupId(match.track.storeId, type); - } - } -}; - -function exactMatch(needle, haystack, type) { - // try to find exact match - return haystack.find(function(entry) { - if (!entry[type]) { - return false; - } - entry = entry[type]; - if (entry.title === needle) { - return entry; - } - }); } -function looseMatch(needle, haystack, type) { - // try to find exact match - return haystack.find(function(entry) { - if (!entry[type]) { - return false; - } - const name = entry[type].title || entry[type].name; - if (name.indexOf(needle) >= 0) { - return entry[type]; - } - }); -} +export const match = urlMatch; +export const id = 'google'; diff --git a/lib/services/google/url.js b/lib/services/google/url.js index e4066c7..30a71b5 100644 --- a/lib/services/google/url.js +++ b/lib/services/google/url.js @@ -1,7 +1,7 @@ import { parse } from 'url'; -export function* match(url) { - var parsed = parse(url.replace(/\+/g, "%20")); +export default function match(url) { + const parsed = parse(url.replace(/\+/g, '%20')); if (!parsed.host.match(/play\.google\.com$/)) { return false; } @@ -10,7 +10,7 @@ export function* match(url) { const hash = parsed.hash; if (hash) { - const parts = hash.split("/"); + const parts = hash.split('/'); const id = parts[2]; const artist = parts[3]; @@ -19,11 +19,11 @@ export function* match(url) { } else if (artist.length > 0) { return true; } - } else if(path) { + } else if (path) { const matches = path.match(/\/music\/m\/([\w]+)/); if (matches[1]) { - return true + return true; } } - return false -}; + return false; +} diff --git a/lib/services/itunes/index.js b/lib/services/itunes/index.js index d78004a..93302df 100644 --- a/lib/services/itunes/index.js +++ b/lib/services/itunes/index.js @@ -2,17 +2,13 @@ import { parse } from 'url'; import querystring from 'querystring'; import request from 'superagent'; import 'superagent-bluebird-promise'; -import { match as urlMatch } from './url'; - -export let id = 'itunes'; +import urlMatch from './url'; const apiRoot = 'https://itunes.apple.com'; -export const match = urlMatch; - export function* parseUrl(url) { const parsed = parse(url); - const matches = parsed.path.match(/[\/]?([\/]?[a-z]{2}?)?[\/]+album[\/]+([^\/]+)[\/]+([^\?]+)/); + const matches = parsed.path.match(/[/]?([/]?[a-z]{2}?)?[/]+album[/]+([^/]+)[/]+([^?]+)/); const query = querystring.parse(parsed.query); if (matches) { @@ -23,20 +19,21 @@ export function* parseUrl(url) { id = query.i; } return yield module.exports.lookupId(id, type, matches[1] || 'us'); - } else { - throw new Error(); } -}; + throw new Error(); +} -export function* lookupId(id, type, cc) { - if (String(id).match(/^[a-z]{2}/)) { - cc = id.substr(0, 2); - id = id.substr(2); +export function* lookupId(possibleId, type, countrycode) { + let cc = countrycode; + let id = possibleId; + if (String(possibleId).match(/^[a-z]{2}/)) { + cc = possibleId.substr(0, 2); + id = possibleId.substr(2); } - let path = '/lookup?id=' + id; + let path = `/lookup?id=${id}`; if (cc) { - path = '/' + cc + path; + path = `/${cc}${path}`; } const response = yield request.get(apiRoot + path); @@ -49,54 +46,56 @@ export function* lookupId(id, type, cc) { } else { result = result.results[0]; - let item = { + const item = { service: 'itunes', - type: type, + type, id: cc + id, name: result.trackName ? result.trackName : result.collectionName, streamUrl: null, purchaseUrl: result.collectionViewUrl, artwork: { - small: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '200x200').replace('http://', ''), - large: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '600x600').replace('http://', '') + small: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '200x200').replace('http://', '')}`, + large: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '600x600').replace('http://', '')}`, }, artist: { - name: result.artistName - } + name: result.artistName, + }, }; if (type === 'track') { item.album = { - name: result.collectionName + name: result.collectionName, }; } return item; } -}; +} export function* search(data) { const markets = ['us', 'gb', 'jp', 'br', 'de', 'es']; - let query, album, entity; + let query; + let album; + let entity; const type = data.type; if (type === 'album') { - query = data.artist.name + ' ' + data.name; + query = `${data.artist.name} ${data.name}`; album = data.name; entity = 'album'; } else if (type === 'track') { - query = data.artist.name + ' ' + data.albumName + ' ' + data.name; + query = `${data.artist.name} ${data.albumName} ${data.name}`; album = data.albumName; entity = 'musicTrack'; } - for (let market of markets) { - const path = '/' + market + '/search?term=' + encodeURIComponent(query) + '&media=music&entity=' + entity; + for (const market of markets) { // eslint-disable-line + const path = `/${market}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}`; const response = yield request.get(apiRoot + path); let result = JSON.parse(response.text); if (!result.results[0]) { - const matches = album.match(/^[^\(\[]+/); + const matches = album.match(/^[^([]+/); if (matches && matches[0] && matches[0] !== album) { const cleanedData = JSON.parse(JSON.stringify(data)); if (type === 'album') { @@ -111,27 +110,30 @@ export function* search(data) { const item = { service: 'itunes', - type: type, - id: 'us' + result.collectionId, + type, + id: `us${result.collectionId}`, name: result.trackName ? result.trackName : result.collectionName, streamUrl: null, purchaseUrl: result.collectionViewUrl, artwork: { - small: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '200x200').replace('http://', ''), - large: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '600x600').replace('http://', '') + small: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '200x200').replace('http://', '')}`, + large: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '600x600').replace('http://', '')}`, }, artist: { - name: result.artistName - } + name: result.artistName, + }, }; if (type === 'track') { item.album = { - name: result.collectionName + name: result.collectionName, }; } return item; } } - return {service: 'itunes'}; -}; + return { service: 'itunes' }; +} + +export const id = 'itunes'; +export const match = urlMatch; diff --git a/lib/services/itunes/url.js b/lib/services/itunes/url.js index 22b3ed6..6392e74 100644 --- a/lib/services/itunes/url.js +++ b/lib/services/itunes/url.js @@ -1,15 +1,13 @@ import { parse } from 'url'; -import querystring from 'querystring'; -export function* match(url, type) { +export default function match(url) { const parsed = parse(url); if (!parsed.host.match(/itunes.apple\.com$/)) { return false; } - const matches = parsed.path.match(/[\/]?([\/]?[a-z]{2}?)?[\/]+album[\/]+([^\/]+)[\/]+([^\?]+)/); - const query = querystring.parse(parsed.query); + const matches = parsed.path.match(/[/]?([/]?[a-z]{2}?)?[/]+album[/]+([^/]+)[/]+([^?]+)/); return !!matches[3]; -}; +} diff --git a/lib/services/spotify/index.js b/lib/services/spotify/index.js index 009dc7b..e470f49 100644 --- a/lib/services/spotify/index.js +++ b/lib/services/spotify/index.js @@ -1,101 +1,122 @@ import { parse } from 'url'; -import bluebird from 'bluebird'; -import spotifyCB from 'spotify'; -import request from 'superagent'; -import 'superagent-bluebird-promise'; -const spotify = bluebird.promisifyAll(spotifyCB); -import { match as urlMatch } from './url'; +import SpotifyWebApi from 'spotify-web-api-node'; +import urlMatch from './url'; -export let id = "spotify"; +const spotify = new SpotifyWebApi({ + clientId: process.env.SPOTIFY_CLIENT_ID, + clientSecret: process.env.SPOTIFY_CLIENT_SECRET, + redirectUri: 'https://match.audio', +}); -export const match = urlMatch; -export function* parseUrl(url) { - var matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/); +function exactMatch(needle, haystack, type) { + // try to find exact match + return haystack.find((entry) => { + if (entry.type !== type) { + return false; + } - if (matches && matches[2]) { - return yield lookupId(matches[2], matches[1]); - } + if (entry.name === needle) { + return entry; + } + return false; + }); +} + +function looseMatch(needle, haystack, type) { + // try to find exact match + return haystack.find((entry) => { + if (entry.type !== type) { + return false; + } + + if (entry.name.indexOf(needle) >= 0) { + return entry; + } + return false; + }); } export function* lookupId(id, type) { - const data = yield spotify.lookupAsync({id: id, type: type}); - if (data.error) { - var error = new Error("Not Found"); - error.status = 404; - throw error; - } + const token = yield spotify.clientCredentialsGrant(); + spotify.setAccessToken(token.body['access_token']); + let data = yield spotify[`get${type.charAt(0).toUpperCase()}${type.slice(1)}s`]([id]); - var artist = data.artists[0]; + data = data.body[`${type}s`][0]; - if (type == "album") { + const artist = data.artists[0]; + + if (type === 'album') { return { - service: "spotify", - type: type, + service: 'spotify', + type, id: data.id, name: data.name, - streamUrl: "https://play.spotify.com/" + type + "/" + data.id, + streamUrl: `https://play.spotify.com/${type}/${data.id}`, purchaseUrl: null, artwork: { - small: data.images[1].url.replace("http:", "https:"), - large: data.images[0].url.replace("http:", "https:"), + small: data.images[1].url.replace('http:', 'https:'), + large: data.images[0].url.replace('http:', 'https:'), }, artist: { - name: artist.name - } + name: artist.name, + }, }; - } else if (type == "track") { + } else if (type === 'track') { return { - service: "spotify", - type: type, + service: 'spotify', + type, id: data.id, name: data.name, - streamUrl: "https://play.spotify.com/" + type + "/" + data.id, + streamUrl: `https://play.spotify.com/${type}/${data.id}`, purchaseUrl: null, artwork: { - small: data.album.images[1].url.replace("http:", "https:"), - large: data.album.images[0].url.replace("http:", "https:"), + small: data.album.images[1].url.replace('http:', 'https:'), + large: data.album.images[0].url.replace('http:', 'https:'), }, artist: { - name: artist.name + name: artist.name, }, album: { - name: data.album.name - } + name: data.album.name, + }, }; } + return { service: 'spotify' }; } -export function* search(data, original={}) { +export function* search(data, original = {}) { + const token = yield spotify.clientCredentialsGrant(); + spotify.setAccessToken(token.body['access_token']); + const markets = ['US', 'GB', 'JP', 'BR', 'DE', 'ES']; - const cleanParam = function(str) { - var chopChars = ['&', '[', '(']; - chopChars.forEach(function(chr) { + function cleanParam(str) { + const chopChars = ['&', '[', '(']; + chopChars.forEach((chr) => { if (data.artist.name.indexOf('&') > 0) { - str = str.substring(0, data.artist.name.indexOf(chr)); + str = str.substring(0, data.artist.name.indexOf(chr)); // eslint-disable-line no-param-reassign,max-len } - }) - return str.replace(/[\:\?]+/, ""); + }); + return str.replace(/[:?]+/, ''); } - let query, album; + let query; const type = data.type; - if (type == "album") { - query = "artist:" + cleanParam(data.artist.name) + " album:" + cleanParam(data.name); - album = data.name; - } else if (type == "track") { - query = "artist:" + cleanParam(data.artist.name) + " track:" + cleanParam(data.name) + ( cleanParam(data.albumName).length > 0 ? " album:" + cleanParam(data.albumName): ""); - album = data.albumName; + if (type === 'album') { + query = `artist:${cleanParam(data.artist.name)} album:${cleanParam(data.name)}`; + } else if (type === 'track') { + query = `artist:${cleanParam(data.artist.name)} track:${cleanParam(data.name)}${cleanParam(data.albumName).length > 0 ? ` album:${cleanParam(data.albumName)}` : ''}`; } - for (let market of markets) { - const response = yield request.get('https://api.spotify.com/v1/search?type=' + type + '&q=' + encodeURI(query) + '&market=' + market); - const items = response.body[type + 's'].items; + for (const market of markets) { // eslint-disable-line + const response = yield spotify[`search${type.charAt(0).toUpperCase()}${type.slice(1)}s`](query, { market }); + + const items = response.body[`${type}s`].items; const name = original.name || data.name; - let match; - if (!(match = exactMatch(name, items, type))) { + let match = exactMatch(name, items, type); + if (!match) { match = looseMatch(name, items, type); } @@ -107,31 +128,17 @@ export function* search(data, original={}) { } } } - return {service: "spotify"}; + return { service: 'spotify' }; } -function exactMatch(needle, haystack, type) { - // try to find exact match - return haystack.find(function(entry) { - if (entry.type !== type) { - return false; - } +export function* parseUrl(url) { + const matches = parse(url).path.match(/\/(album|track)[/]+([^/]+)/); - if (entry.name === needle) { - return entry; - } - }); + if (matches && matches[2]) { + return yield lookupId(matches[2], matches[1]); + } + throw new Error(); } -function looseMatch(needle, haystack, type) { - // try to find exact match - return haystack.find(function(entry) { - if (entry.type !== type) { - return false; - } - - if (entry.name.indexOf(needle) >= 0) { - return entry - } - }); -} +export const id = 'spotify'; +export const match = urlMatch; diff --git a/lib/services/spotify/url.js b/lib/services/spotify/url.js index 0ae7c77..61cdb44 100644 --- a/lib/services/spotify/url.js +++ b/lib/services/spotify/url.js @@ -1,11 +1,11 @@ import { parse } from 'url'; -export function* match(url, type) { +export default function match(url) { const parsed = parse(url); if (!parsed.host.match(/spotify\.com$/)) { return false; } - const matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/); + const matches = parse(url).path.match(/\/(album|track)[/]+([^/]+)/); return matches && !!matches[2]; -}; +} diff --git a/lib/services/xbox/index.js b/lib/services/xbox/index.js index d1a8649..62ddb84 100644 --- a/lib/services/xbox/index.js +++ b/lib/services/xbox/index.js @@ -1,21 +1,18 @@ import { parse } from 'url'; -import querystring from 'querystring'; import request from 'superagent'; import 'superagent-bluebird-promise'; -import { match as urlMatch } from './url'; - import debuglog from 'debug'; +import urlMatch from './url'; + const debug = debuglog('match.audio:xbox'); -export let id = 'xbox'; - if (!process.env.XBOX_CLIENT_ID || !process.env.XBOX_CLIENT_SECRET) { - console.warn('XBOX_CLIENT_ID and XBOX_CLIENT_SECRET environment variables not found, deactivating Xbox Music.'); + debug('XBOX_CLIENT_ID and XBOX_CLIENT_SECRET environment variables not found, deactivating Xbox Music.'); } const credentials = { clientId: process.env.XBOX_CLIENT_ID, - clientSecret: process.env.XBOX_CLIENT_SECRET + clientSecret: process.env.XBOX_CLIENT_SECRET, }; const apiRoot = 'https://music.xboxlive.com/1/content'; @@ -28,10 +25,14 @@ function* getAccessToken() { const data = { client_id: credentials.clientId, client_secret: credentials.clientSecret, - scope: scope, - grant_type: grantType + scope, + grant_type: grantType, }; - const result = yield request.post(authUrl).timeout(10000).send(data).set('Content-type', 'application/x-www-form-urlencoded').promise(); + const result = yield request.post(authUrl) + .timeout(10000) + .send(data) + .set('Content-type', 'application/x-www-form-urlencoded') + .promise(); return result.body.access_token; } @@ -44,41 +45,27 @@ function formatResponse(match) { streamUrl: match.Link, purchaseUrl: null, artwork: { - small: match.ImageUrl.replace('http://', 'https://') + '&w=250&h=250', - large: match.ImageUrl.replace('http://', 'https://') + '&w=500&h=500' + small: `${match.ImageUrl.replace('http://', 'https://')}&w=250&h=250`, + large: `${match.ImageUrl.replace('http://', 'https://')}&w=500&h=500`, }, artist: { - name: match.Artists[0].Artist.Name - } + name: match.Artists[0].Artist.Name, + }, }; if (match.Album) { - item.album = {name: match.Album.Name} + item.album = { name: match.Album.Name }; } return item; } function* apiCall(path) { - const access_token = yield getAccessToken(); - return request.get(apiRoot + path).timeout(10000).set('Authorization', 'Bearer ' + access_token).promise(); -} - -export const match = urlMatch; - -export function* parseUrl(url) { - const parsed = parse(url); - const parts = parsed.path.split('/'); - const type = parts[1]; - const idMatches = parts[4].match(/bz.[\w\-]+/); - const id = idMatches[0].replace('bz.', 'music.'); - if (!id) { - return false; - } - return yield lookupId(id, type); + const accessToken = yield getAccessToken(); + return request.get(apiRoot + path).timeout(10000).set('Authorization', `Bearer ${accessToken}`).promise(); } export function* lookupId(id, type) { - const path = '/' + id + '/lookup'; - const apiType = type.charAt(0).toUpperCase() + type.substr(1) + 's'; + const path = `/${id}/lookup`; + const apiType = `${type.charAt(0).toUpperCase() + type.substr(1)}s`; try { const result = yield apiCall(path); return formatResponse(result.body[apiType].Items[0]); @@ -86,31 +73,61 @@ export function* lookupId(id, type) { if (e.status !== 404) { debug(e.body); } - return {service: 'xbox'}; + return { service: 'xbox' }; } -}; +} + +export function* parseUrl(url) { + const parsed = parse(url); + const parts = parsed.path.split('/'); + const type = parts[1]; + const idMatches = parts[4].match(/bz.[\w-]+/); + const id = idMatches[0].replace('bz.', 'music.'); + if (!id) { + return false; + } + return yield lookupId(id, type); +} + +function exactMatch(item, artist, haystack) { + // try to find exact match + return haystack.find((entry) => { + if (entry.Name === item && entry.Artists[0].Artist.Name === artist) { + return entry; + } + return false; + }); +} + +function looseMatch(item, artist, haystack) { + // try to find exact match + return haystack.find((entry) => { + if (entry.Name.indexOf(item) >= 0 && entry.Artists[0].Artist.Name.indexOf(artist) >= 0) { + return entry; + } + return false; + }); +} export function* search(data) { - var cleanParam = function(str) { - return str.replace(/[\:\?\&\(\)\[\]]+/g, ''); + function cleanParam(str) { + return str.replace(/[:?&()[\]]+/g, ''); } - let query, album; + let query; const type = data.type; - if (type == 'album') { - query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name); - album = data.name; - } else if (type == 'track') { - query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name); - album = data.albumName + if (type === 'album') { + query = `${cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&')))} ${cleanParam(data.name)}`; + } else if (type === 'track') { + query = `${cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&')))} ${cleanParam(data.name)}`; } const name = data.name; - const path = '/music/search?q=' + encodeURIComponent(query.trim()) + '&filters=' + type + 's'; + const path = `/music/search?q=${encodeURIComponent(query.trim())}&filters=${type}s`; try { const result = yield apiCall(path); - const apiType = type.charAt(0).toUpperCase() + type.substr(1) + 's'; + const apiType = `${type.charAt(0).toUpperCase() + type.substr(1)}s`; let match = exactMatch(name, data.artist.name, result.body[apiType].Items, type); if (!match) { @@ -121,24 +138,10 @@ export function* search(data) { return formatResponse(match); } } catch (err) { - return {service: 'xbox'}; + return { service: 'xbox' }; } -}; - -function exactMatch(item, artist, haystack, type) { - // try to find exact match - return haystack.find(function(entry) { - if (entry.Name === item && entry.Artists[0].Artist.Name === artist) { - return entry; - } - }); + return { service: 'xbox' }; } -function looseMatch(item, artist, haystack, type) { - // try to find exact match - return haystack.find(function(entry) { - if (entry.Name.indexOf(item) >= 0 && entry.Artists[0].Artist.Name.indexOf(artist) >= 0) { - return entry; - } - }); -} +export const id = 'xbox'; +export const match = urlMatch; diff --git a/lib/services/xbox/url.js b/lib/services/xbox/url.js index a545dd1..bae28d6 100644 --- a/lib/services/xbox/url.js +++ b/lib/services/xbox/url.js @@ -1,11 +1,11 @@ import { parse } from 'url'; -export function* match(url, type) { +export default function match(url) { const parsed = parse(url); if (!parsed.host.match(/music.microsoft.com$/)) { return false; } const parts = parsed.path.split('/'); - return (parts[1] == 'album' || parts[1] == 'track') && parts[4]; -}; + return (parts[1] === 'album' || parts[1] === 'track') && parts[4]; +} diff --git a/lib/services/youtube/freebase.js b/lib/services/youtube/freebase.js deleted file mode 100644 index ef2377d..0000000 --- a/lib/services/youtube/freebase.js +++ /dev/null @@ -1,15 +0,0 @@ -import { parse } from 'url'; -import querystring from 'querystring'; -import request from 'superagent'; -import 'superagent-bluebird-promise'; - -const credentials = { - key: process.env.YOUTUBE_KEY, -}; - -const apiRoot = 'https://www.googleapis.com/freebase/v1/topic'; - -export function* get(topic) { - const result = yield request.get(apiRoot + topic + '?key=' + credentials.key).promise(); - return result.body; -} diff --git a/lib/services/youtube/index.js b/lib/services/youtube/index.js index 2b00e65..95a4640 100644 --- a/lib/services/youtube/index.js +++ b/lib/services/youtube/index.js @@ -1,18 +1,15 @@ import { parse } from 'url'; import querystring from 'querystring'; -import moment from 'moment'; import request from 'superagent'; +import Nodebrainz from 'nodebrainz'; import 'superagent-bluebird-promise'; -import { match as urlMatch } from './url'; -import freebase from './freebase'; - import debuglog from 'debug'; +import urlMatch from './url'; + const debug = debuglog('match.audio:youtube'); -module.exports.id = 'youtube'; - if (!process.env.YOUTUBE_KEY) { - console.warn('YOUTUBE_KEY environment variable not found, deactivating Youtube.'); + debug('YOUTUBE_KEY environment variable not found, deactivating Youtube.'); } const credentials = { @@ -21,7 +18,77 @@ const credentials = { const apiRoot = 'https://www.googleapis.com/youtube/v3'; -export const match = urlMatch; +const nodebrainz = new Nodebrainz({ + userAgent: 'match-audio ( https://match.audio )', + defaultLimit: 10, + retryOn: true, + retryDelay: 3000, + retryCount: 10, +}); + +export function* lookupId(id) { + const path = `/videos?part=snippet%2CtopicDetails%2CcontentDetails&id=${id}&key=${credentials.key}`; + try { + const result = yield request.get(apiRoot + path).promise(); + const item = result.body.items[0]; + + nodebrainz.luceneSearch('release', { query: item.snippet.title }, (err, response) => { + response.releases.forEach((release) => { + //console.log(release); + }); + }); + + const match = { + id, + service: 'youtube', + name: item.snippet.title, + type: 'track', + album: { name: '' }, + streamUrl: `https://youtu.be/${id}`, + purchaseUrl: null, + artwork: { + small: item.snippet.thumbnails.medium.url, + large: item.snippet.thumbnails.high.url, + }, + }; + + return match; + } catch (err) { + debug(err); + return { service: 'youtube' }; + } +} + +export function* search(data) { + let query; + const type = data.type; + + if (type === 'album') { + query = `${data.artist.name} ${data.name}`; + } else if (type === 'track') { + query = `${data.artist.name} ${data.name}`; + } + + const path = `/search?part=snippet&q=${encodeURIComponent(query)}&type=video&videoCaption=any&videoCategoryId=10&key=${credentials.key}`; + const result = yield request.get(apiRoot + path).promise(); + const item = result.body.items[0]; + + if (!item) { + return { service: 'youtube', type: 'video' }; + } + return { + service: 'youtube', + type: 'video', + id: item.id.videoId, + name: item.snippet.title, + streamUrl: `https://www.youtube.com/watch?v=${item.id.videoId}`, + purchaseUrl: null, + artwork: { + small: item.snippet.thumbnails.medium.url, + large: item.snippet.thumbnails.high.url, + }, + }; +} export function parseUrl(url) { const parsed = parse(url); @@ -37,93 +104,5 @@ export function parseUrl(url) { return lookupId(id, 'track'); } -export function* lookupId(id, type) { - - const path = '/videos?part=snippet%2CtopicDetails%2CcontentDetails&id=' + id + '&key=' + credentials.key; - try { - const result = yield request.get(apiRoot + path).promise(); - const item = result.body.items[0]; - if (!item.topicDetails.topicIds) { - return {service: 'youtube'}; - } - - const match = { - id: id, - service: 'youtube', - name: item.snippet.title, - type: 'track', - album: {name: ''}, - streamUrl: 'https://youtu.be/' + id, - purchaseUrl: null, - artwork: { - small: item.snippet.thumbnails.medium.url, - large: item.snippet.thumbnails.high.url, - } - }; - - for (let topic of yield freebase.get(topicId)) { - const musicalArtist = topic.property['/type/object/type'].values.some((value) => { - return value.text == 'Musical Artist'; - }); - - const musicalRecording = topic.property['/type/object/type'].values.some(function(value) { - return value.text == 'Musical Recording'; - }); - - const musicalAlbum = topic.property['/type/object/type'].values.some(function(value) { - return value.text == 'Musical Album'; - }) - - if (musicalArtist) { - match.artist = {name: topic.property['/type/object/name'].values[0].text}; - } else if (musicalRecording) { - match.name = topic.property['/type/object/name'].values[0].text; - if (topic.property['/music/recording/releases']) { - match.type = 'album'; - match.albumName = topic.property['/music/recording/releases'].values[0].text; - } - } else if (musicalAlbum) { - match.name = topic.property['/type/object/name'].values[0].text; - match.type = 'album'; - } - } - return match; - } catch (e) { - debug(e.body); - return {'service': 'youtube'}; - } -}; - -export function* search(data) { - let query, album; - const type = data.type; - - if (type == 'album') { - query = data.artist.name + ' ' + data.name; - album = data.name; - } else if (type == 'track') { - query = data.artist.name + ' ' + data.name; - album = data.albumName - } - - const path = '/search?part=snippet&q=' + encodeURIComponent(query) + '&type=video&videoCaption=any&videoCategoryId=10&key=' + credentials.key; - const result = yield request.get(apiRoot + path).promise(); - const item = result.body.items[0]; - - if (!item) { - return {service:'youtube', type: 'video'}; - } else { - return { - service: 'youtube', - type: 'video', - id: item.id.videoId, - name: item.snippet.title, - streamUrl: 'https://www.youtube.com/watch?v=' + item.id.videoId, - purchaseUrl: null, - artwork: { - small: item.snippet.thumbnails.medium.url, - large: item.snippet.thumbnails.high.url, - } - }; - } -}; +export const id = 'youtube'; +export const match = urlMatch; diff --git a/lib/services/youtube/url.js b/lib/services/youtube/url.js index 3ea5236..116eb3a 100644 --- a/lib/services/youtube/url.js +++ b/lib/services/youtube/url.js @@ -1,7 +1,7 @@ import { parse } from 'url'; import querystring from 'querystring'; -export function* match(url, type) { +export default function match(url) { const parsed = parse(url); if (parsed.host.match(/youtu\.be$/)) { return true; @@ -10,4 +10,4 @@ export function* match(url, type) { return !!query.v; } return false; -}; +} diff --git a/package.json b/package.json index 5869cf6..45043f6 100644 --- a/package.json +++ b/package.json @@ -70,11 +70,12 @@ "koa-websocket": "^2.1.0", "moment": "^2.14.1", "node-uuid": "~1.4.2", + "nodebrainz": "^2.1.1", "pg": "^6.1.0", - "playmusic": "~2.2.1", + "playmusic": "https://github.com/jamon/playmusic.git#37e98f39c33fc5359a8a30b8c8e422161a4be9a8", "raven": "^2.0.2", "sequelize": "^3.24.3", - "spotify": "~0.3.0", + "spotify-web-api-node": "^2.4.0", "style-loader": "^0.17.0", "superagent": "^2.1.0", "superagent-bluebird-promise": "^3.0.2", diff --git a/test/services/deezer.js b/test/services/deezer.js index f86d326..cba1d72 100644 --- a/test/services/deezer.js +++ b/test/services/deezer.js @@ -21,8 +21,8 @@ describe('Deezer', function(){ }); it('should find album with various artists by search', function* (){ - const result = yield deezer.search({type: 'album', artist: {name: 'Various Artists'}, name: 'The Trevor Nelson Collection'}); - result.name.should.equal('The Trevor Nelson Collection'); + const result = yield deezer.search({type: 'album', artist: {name: 'Various Artists'}, name: 'The Trevor Nelson Collection 2'}); + result.name.should.equal('The Trevor Nelson Collection 2'); }); it('should find track by search', function* (){ diff --git a/test/services/xbox.js b/test/services/xbox.js index dd60951..9477682 100644 --- a/test/services/xbox.js +++ b/test/services/xbox.js @@ -29,7 +29,7 @@ describe('Xbox Music', function(){ describe('lookupUrl', function(){ it('should parse regular url into album ID', function* (){ - const result = yield xbox.parseUrl('https://music.xbox.com/album/kyuss/muchas-gracias-the-best-of-kyuss/8b558d00-0100-11db-89ca-0019b92a3933'); + const result = yield xbox.parseUrl('https://music.microsoft.com/album/kyuss/muchas-gracias-the-best-of-kyuss/bz.8b558d00-0100-11db-89ca-0019b92a3933'); result.id.should.equal('music.8D6KGX5BZ8WB'); }); }); diff --git a/test/services/youtube.js b/test/services/youtube.js index a33ef79..c80a41e 100644 --- a/test/services/youtube.js +++ b/test/services/youtube.js @@ -2,6 +2,13 @@ import 'should'; import * as youtube from '../../lib/services/youtube'; describe('Youtube', function(){ + describe('lookup', function(){ + it('should find album by lookup', function* (){ + const result = yield youtube.lookupId('6JnGBs88sL0'); + result.name.should.equal('Nelly Furtado - Say It Right'); + }); + }); + describe('search', function(){ it('should find album by search', function* (){ const result = yield youtube.search({type: 'track', artist: {name: 'Aesop Rock'}, album: {name: 'Skeconsthon'}, name: 'Zero Dark Thirty'}); diff --git a/yarn.lock b/yarn.lock index a6e9694..f983e60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3317,6 +3317,10 @@ node-uuid@~1.4.1, node-uuid@~1.4.2: version "1.4.8" resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" +nodebrainz@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nodebrainz/-/nodebrainz-2.1.1.tgz#debf0cbf69ffeaec7439a36409ed9c10404b112f" + nodemon@^1.10.2: version "1.11.0" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c" @@ -3669,12 +3673,13 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" -playmusic@~2.2.1: +"playmusic@https://github.com/jamon/playmusic.git#37e98f39c33fc5359a8a30b8c8e422161a4be9a8": version "2.2.1" - resolved "https://registry.yarnpkg.com/playmusic/-/playmusic-2.2.1.tgz#32c5a4f3dee6e350e61ca879d4ce779f8e801a07" + resolved "https://github.com/jamon/playmusic.git#37e98f39c33fc5359a8a30b8c8e422161a4be9a8" dependencies: crypto-js ">= 3.1" node-uuid "~1.4.1" + rsa-pem-from-mod-exp "^0.8.4" pluralize@^1.2.1: version "1.2.1" @@ -4333,6 +4338,10 @@ ripemd160@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" +rsa-pem-from-mod-exp@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz#362a42c6d304056d493b3f12bceabb2c6576a6d4" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -4587,9 +4596,11 @@ split@^1.0.0: dependencies: through "2" -spotify@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/spotify/-/spotify-0.3.0.tgz#42b85105cfc30f174c050f2227c21a9d7edb1be4" +spotify-web-api-node@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/spotify-web-api-node/-/spotify-web-api-node-2.4.0.tgz#948f5bcfe098e5027367361dd2b003c7e3ce4cd5" + dependencies: + superagent "^2.0.0" sprintf-js@~1.0.2: version "1.0.3" @@ -4714,7 +4725,7 @@ superagent-bluebird-promise@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/superagent-bluebird-promise/-/superagent-bluebird-promise-3.0.2.tgz#3562fc7f26fe07306119ca8ab9943e1571b1deec" -superagent@^2.1.0: +superagent@^2.0.0, superagent@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/superagent/-/superagent-2.3.0.tgz#703529a0714e57e123959ddefbce193b2e50d115" dependencies: @@ -4984,7 +4995,7 @@ util@0.10.3, util@^0.10.3: dependencies: inherits "2.0.1" -uuid@3.0.0: +uuid@3.0.0, uuid@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.0.tgz#6728fc0459c450d796a99c31837569bdf672d728" @@ -4992,10 +5003,6 @@ uuid@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" - v8flags@^2.0.10: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"