/* * Copyright (c) 2016 Gabriel Augendre * Free software under MIT License. See LICENSE file. */ /** * The functions tagged as "From Spotify tutorial" * are from an example of a basic node.js script that performs * the Authorization Code oAuth2 flow to authenticate against * the Spotify Accounts. * * It's licensed under Apache 2.0. * See https://github.com/spotify/web-api-auth-examples for more information. * * For more information about the tutorial, read * https://developer.spotify.com/web-api/authorization-guide/#authorization_code_flow */ 'use strict'; var express = require('express'); // Express web server framework var request = require('request'); // "Request" library var querystring = require('querystring'); var cookieParser = require('cookie-parser'); var enforce = require('express-sslify'); var client_id = process.env.CLIENT_ID; // Your client id var client_secret = process.env.CLIENT_SECRET; // Your client secret var redirect_uri = process.env.CALLBACK; // Your redirect uri var environment = process.env.NODE_ENV || 'prod'; /** * Generates a random string containing numbers and letters. * From Spotify tutorial. * @param {number} length The length of the string * @return {string} The generated string */ var generateRandomString = function (length) { var text = ''; var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (var i = 0; i < length; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; }; var stateKey = 'spotify_auth_state'; var app = express(); if (environment == 'prod' || environment == 'production') { app.use(enforce.HTTPS({trustProtoHeader: true})); } app.use(express.static(__dirname + '/public')) .use(cookieParser()); /** * From Spotify tutorial. */ app.get('/login', function (req, res) { var state = generateRandomString(16); res.cookie(stateKey, state); // your application requests authorization // Changed scopes from Spotify tytorial var scope = 'playlist-read-private playlist-read-collaborative'; res.redirect('https://accounts.spotify.com/authorize?' + querystring.stringify({ response_type: 'code', client_id: client_id, scope: scope, redirect_uri: redirect_uri, state: state })); }); /** * From Spotify tutorial. */ app.get('/callback', function (req, res) { // your application requests refresh and access tokens // after checking the state parameter var code = req.query.code || null; var state = req.query.state || null; var storedState = req.cookies ? req.cookies[stateKey] : null; if (state === null || state !== storedState) { res.redirect('/#' + querystring.stringify({ error: 'state_mismatch' })); } else { res.clearCookie(stateKey); var authOptions = { url: 'https://accounts.spotify.com/api/token', form: { code: code, redirect_uri: redirect_uri, grant_type: 'authorization_code' }, headers: { 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) }, json: true }; request.post(authOptions, function (error, response, body) { if (!error && response.statusCode === 200) { var access_token = body.access_token, refresh_token = body.refresh_token; // we can also pass the token to the browser to make requests from there res.redirect('/#' + querystring.stringify({ access_token: access_token, refresh_token: refresh_token })); } else { res.redirect('/#' + querystring.stringify({ error: 'invalid_token' })); } }); } }); /** * From Spotify tutorial. */ app.get('/refresh_token', function (req, res) { // requesting access token from refresh token var refresh_token = req.query.refresh_token; var authOptions = { url: 'https://accounts.spotify.com/api/token', headers: {'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64'))}, form: { grant_type: 'refresh_token', refresh_token: refresh_token }, json: true }; request.post(authOptions, function (error, response, body) { if (!error && response.statusCode === 200) { var access_token = body.access_token; res.send({ 'access_token': access_token }); } }); }); app.get('/get_playlists', function (req, res) { // requesting access token from refresh token var access_token = req.query.access_token; var next = req.query.next; var authOptions = { url: next ? next : 'https://api.spotify.com/v1/me/playlists?' + querystring.stringify({ limit: 50 }), headers: {'Authorization': 'Bearer ' + access_token}, json: true }; getAllPages(authOptions, [], function (data) { res.send({ 'data': data }); }); }); app.get('/pl/:uId/:plId', function (req, res) { var plId = req.params.plId; var userId = req.params.uId; var access_token = req.query.access_token; var fields = querystring.stringify({ fields: 'items(track(id,name,artists(id,name))),next' }); var authOptions = { url: 'https://api.spotify.com/v1/users/' + userId + '/playlists/' + plId + '/tracks?' + fields, headers: {'Authorization': 'Bearer ' + access_token}, json: true }; getAllPages(authOptions, [], function (data) { var dups = []; data.forEach(function (item, index, array) { var i = index + 1; while (i < array.length) { var other = array[i]; if (areDups(item.track, other.track)) { dups.push(item); } i++; } }); res.send({ 'data': dups }); }); }); /** * @typedef {Object} track * @property {number|null} id * @property {string} name * @property {[artist]} artists */ /** * @typedef {Object} artist * @property {number} id * @property {string} name */ /** * Check if two tracks are duplicates * @param {track} track1 * @param {track} track2 * @return {boolean} True if the two are duplicates. False otherwise */ function areDups(track1, track2) { var name1 = track1.name.toLowerCase(); var name2 = track2.name.toLowerCase(); if (track1.id != null && track2.id != null && track1.id == track2.id) { return true; } else if (haveCommonArtist(track1, track2)) { if (name1 == name2) { return true; } else if (name1.startsWith(name2) || name2.startsWith(name1)) { return true; } } return false; } /** * Check if two tracks have at least one artist in common based on artist id. * @param {track} track1 * @param {track} track2 * @return {boolean} True if they have at least one artist in common. False otherwise. */ function haveCommonArtist(track1, track2) { return track1.artists[0].id == track2.artists[0].id; } function getAllPages(authOptions, data, callback) { request.get(authOptions, function (error, response, body) { if (!error && response.statusCode === 200) { if (body.next) { authOptions.url = body.next; getAllPages(authOptions, data.concat(body.items), callback); } else { callback(data.concat(body.items)); } } }); } module.exports = app;