Compare commits

...

5 Commits

Author SHA1 Message Date
0685701355 Compiled protobuf 2023-06-24 12:56:38 +00:00
2b6825b7e3 Compiling protobuf 2023-06-24 12:43:10 +00:00
0bfe341afe Adding some features 2023-06-24 14:25:44 +02:00
4419482152 Adding the request models 2023-06-24 12:15:46 +02:00
e1a0fe68e8 Creating the main files 2023-06-24 12:05:36 +02:00
9 changed files with 2052 additions and 1 deletions

3
.gitignore vendored
View File

@@ -0,0 +1,3 @@
node_modules
.env
package-lock.json

View File

@@ -7,4 +7,10 @@ Ce projet a pour objectif de réaliser des actions via l'API de Spotify afin de
- Récupérer des informations sur les musiques, playlists et podcasts
- Contrôler le lecteur de musique
- Visualiser les canvas d'éléments de Spotify
- Récupérer des informations supplémentaires non-affichées dans l'application
- Récupérer des informations supplémentaires non-affichées dans l'application
## Utilisation
Afin d'utiliser ce module, il est nécessaire de créer un fichier `.env` à la racine du projet contenant les variables d'environnement suivantes :
`SPOTIFY_CLIENT_ID='<your-spotify-client-id>'`

13
main.js Normal file
View File

@@ -0,0 +1,13 @@
const SpotifyController = require('./src/spotify.js')
var spotifyController = new SpotifyController().then(initializingSuccess, printError)
// SpotifyController initialized
function initializingSuccess(result) {
console.log(result)
}
// Print the error to the console
function printError(error) {
console.log(error)
}

1249
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "spotifycontroller",
"version": "0.1.0",
"description": "Get all the spotify data with NodeJS!",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://factory.vbprojects.fr/thinkode/SpotifyController.git"
},
"author": "V. Boulanger",
"license": "ISC",
"dependencies": {
"axios": "^1.4.0",
"base64url": "^3.0.1",
"crypto": "^1.0.1",
"express": "^4.18.2",
"opn": "^6.0.0",
"randomstring": "^1.3.0"
}
}

16
src/configuration.js Normal file
View File

@@ -0,0 +1,16 @@
const SpotifyEndpoints = {
GET_USER_TOKEN_ENDPOINT: "https://accounts.spotify.com/api/token",
GET_USER_PROFILE_ENDPOINT: 'https://api.spotify.com/v1/me'
}
const AuthorizationScopes = [
'user-read-private',
'user-read-email',
'user-top-read',
'user-read-currently-playing'
]
module.exports = {
SpotifyEndpoints,
AuthorizationScopes
};

31
src/protobuf/api.proto Normal file
View File

@@ -0,0 +1,31 @@
syntax = "proto3";
package com.spotify.api;
// Request used to get a new token for the current user
message GetUserTokenRequest {
// The client id
string client_id = 1;
// The grant type
string grant_type = 2;
// The code returned by the authorization server
string code = 3;
// The redirect uri
string redirect_uri = 4;
// The client secret
string code_verifier = 5;
}
// Reponse of the GetUserTokenRequest
message GetUserTokenResponse {
// The user access token
string access_token = 1;
// The token type
string token_type = 2;
// The time in seconds before the access token expires
int32 expires_in = 3;
// The token used to refresh the user token
string refresh_token = 4;
// A space-separated list of scopes which have been granted for this access_token
string scope = 5;
}

568
src/protobuf/api_pb.js Normal file
View File

@@ -0,0 +1,568 @@
// source: src/protobuf/api.proto
/**
* @fileoverview
* @enhanceable
* @suppress {missingRequire} reports error on implicit type usages.
* @suppress {messageConventions} JS Compiler reports an error if a variable or
* field starts with 'MSG_' and isn't a translatable message.
* @public
*/
// GENERATED CODE -- DO NOT EDIT!
/* eslint-disable */
// @ts-nocheck
var jspb = require('google-protobuf');
var goog = jspb;
var global =
(typeof globalThis !== 'undefined' && globalThis) ||
(typeof window !== 'undefined' && window) ||
(typeof global !== 'undefined' && global) ||
(typeof self !== 'undefined' && self) ||
(function () { return this; }).call(null) ||
Function('return this')();
goog.exportSymbol('proto.com.spotify.api.GetUserTokenRequest', null, global);
goog.exportSymbol('proto.com.spotify.api.GetUserTokenResponse', null, global);
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.com.spotify.api.GetUserTokenRequest = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.com.spotify.api.GetUserTokenRequest, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.com.spotify.api.GetUserTokenRequest.displayName = 'proto.com.spotify.api.GetUserTokenRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.com.spotify.api.GetUserTokenResponse = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.com.spotify.api.GetUserTokenResponse, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.com.spotify.api.GetUserTokenResponse.displayName = 'proto.com.spotify.api.GetUserTokenResponse';
}
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.toObject = function(opt_includeInstance) {
return proto.com.spotify.api.GetUserTokenRequest.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.com.spotify.api.GetUserTokenRequest} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.com.spotify.api.GetUserTokenRequest.toObject = function(includeInstance, msg) {
var f, obj = {
clientId: jspb.Message.getFieldWithDefault(msg, 1, ""),
grantType: jspb.Message.getFieldWithDefault(msg, 2, ""),
code: jspb.Message.getFieldWithDefault(msg, 3, ""),
redirectUri: jspb.Message.getFieldWithDefault(msg, 4, ""),
codeVerifier: jspb.Message.getFieldWithDefault(msg, 5, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.com.spotify.api.GetUserTokenRequest}
*/
proto.com.spotify.api.GetUserTokenRequest.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.com.spotify.api.GetUserTokenRequest;
return proto.com.spotify.api.GetUserTokenRequest.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.com.spotify.api.GetUserTokenRequest} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.com.spotify.api.GetUserTokenRequest}
*/
proto.com.spotify.api.GetUserTokenRequest.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setClientId(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setGrantType(value);
break;
case 3:
var value = /** @type {string} */ (reader.readString());
msg.setCode(value);
break;
case 4:
var value = /** @type {string} */ (reader.readString());
msg.setRedirectUri(value);
break;
case 5:
var value = /** @type {string} */ (reader.readString());
msg.setCodeVerifier(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.com.spotify.api.GetUserTokenRequest.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.com.spotify.api.GetUserTokenRequest} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.com.spotify.api.GetUserTokenRequest.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getClientId();
if (f.length > 0) {
writer.writeString(
1,
f
);
}
f = message.getGrantType();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
f = message.getCode();
if (f.length > 0) {
writer.writeString(
3,
f
);
}
f = message.getRedirectUri();
if (f.length > 0) {
writer.writeString(
4,
f
);
}
f = message.getCodeVerifier();
if (f.length > 0) {
writer.writeString(
5,
f
);
}
};
/**
* optional string client_id = 1;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.getClientId = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenRequest} returns this
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.setClientId = function(value) {
return jspb.Message.setProto3StringField(this, 1, value);
};
/**
* optional string grant_type = 2;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.getGrantType = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenRequest} returns this
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.setGrantType = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
/**
* optional string code = 3;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.getCode = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenRequest} returns this
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.setCode = function(value) {
return jspb.Message.setProto3StringField(this, 3, value);
};
/**
* optional string redirect_uri = 4;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.getRedirectUri = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenRequest} returns this
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.setRedirectUri = function(value) {
return jspb.Message.setProto3StringField(this, 4, value);
};
/**
* optional string code_verifier = 5;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.getCodeVerifier = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenRequest} returns this
*/
proto.com.spotify.api.GetUserTokenRequest.prototype.setCodeVerifier = function(value) {
return jspb.Message.setProto3StringField(this, 5, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.toObject = function(opt_includeInstance) {
return proto.com.spotify.api.GetUserTokenResponse.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.com.spotify.api.GetUserTokenResponse} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.com.spotify.api.GetUserTokenResponse.toObject = function(includeInstance, msg) {
var f, obj = {
accessToken: jspb.Message.getFieldWithDefault(msg, 1, ""),
tokenType: jspb.Message.getFieldWithDefault(msg, 2, ""),
expiresIn: jspb.Message.getFieldWithDefault(msg, 3, 0),
refreshToken: jspb.Message.getFieldWithDefault(msg, 4, ""),
scope: jspb.Message.getFieldWithDefault(msg, 5, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.com.spotify.api.GetUserTokenResponse}
*/
proto.com.spotify.api.GetUserTokenResponse.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.com.spotify.api.GetUserTokenResponse;
return proto.com.spotify.api.GetUserTokenResponse.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.com.spotify.api.GetUserTokenResponse} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.com.spotify.api.GetUserTokenResponse}
*/
proto.com.spotify.api.GetUserTokenResponse.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setAccessToken(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setTokenType(value);
break;
case 3:
var value = /** @type {number} */ (reader.readInt32());
msg.setExpiresIn(value);
break;
case 4:
var value = /** @type {string} */ (reader.readString());
msg.setRefreshToken(value);
break;
case 5:
var value = /** @type {string} */ (reader.readString());
msg.setScope(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.com.spotify.api.GetUserTokenResponse.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.com.spotify.api.GetUserTokenResponse} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.com.spotify.api.GetUserTokenResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getAccessToken();
if (f.length > 0) {
writer.writeString(
1,
f
);
}
f = message.getTokenType();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
f = message.getExpiresIn();
if (f !== 0) {
writer.writeInt32(
3,
f
);
}
f = message.getRefreshToken();
if (f.length > 0) {
writer.writeString(
4,
f
);
}
f = message.getScope();
if (f.length > 0) {
writer.writeString(
5,
f
);
}
};
/**
* optional string access_token = 1;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.getAccessToken = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenResponse} returns this
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.setAccessToken = function(value) {
return jspb.Message.setProto3StringField(this, 1, value);
};
/**
* optional string token_type = 2;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.getTokenType = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenResponse} returns this
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.setTokenType = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
/**
* optional int32 expires_in = 3;
* @return {number}
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.getExpiresIn = function() {
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0));
};
/**
* @param {number} value
* @return {!proto.com.spotify.api.GetUserTokenResponse} returns this
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.setExpiresIn = function(value) {
return jspb.Message.setProto3IntField(this, 3, value);
};
/**
* optional string refresh_token = 4;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.getRefreshToken = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenResponse} returns this
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.setRefreshToken = function(value) {
return jspb.Message.setProto3StringField(this, 4, value);
};
/**
* optional string scope = 5;
* @return {string}
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.getScope = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, ""));
};
/**
* @param {string} value
* @return {!proto.com.spotify.api.GetUserTokenResponse} returns this
*/
proto.com.spotify.api.GetUserTokenResponse.prototype.setScope = function(value) {
return jspb.Message.setProto3StringField(this, 5, value);
};
goog.object.extend(exports, proto.com.spotify.api);

142
src/spotify.js Normal file
View File

@@ -0,0 +1,142 @@
const axios = require('axios')
const dotenv = require('dotenv')
const configuration = require('./configuration.js')
const randomstring = require('randomstring')
const base64url = require('base64url')
const crypto = require('crypto')
const opn = require('opn')
const express = require('express')
class SpotifyController {
// Creates a new SpotifyController instance with the given clientId
constructor() {
return new Promise((resolve, reject) => {
dotenv.config()
// For connection management
this.clientId = process.env.SPOTIFY_CLIENT_ID
// For user token management
this.userToken = undefined
this.userTokenExpiringDate = undefined
this.refreshToken = undefined
// For canvas token management
this.canvasToken = undefined
this.canvasTokenExpiringDate = undefined
// Check client ID
if (this.clientId === undefined || this.clientId === null || this.clientId === '') {
reject('Spotify client ID not set. Please define this variable in the .env file.')
}
this.checkUserToken().then(resolve("User token got."), reject("Error requesting a user token"))
resolve("SpotifyController initialized.")
})
}
// This function aims to get a valid user token for the Spotify API
async checkUserToken() {
return new Promise((resolve, reject) => {
if (this.userToken === undefined) {
// Get a new token with auth
this.requestAuthentication().then(resolve("User token requested"), reject("Error requesting user token"))
} else if (this.userTokenExpiringDate < new Date()) {
// Refresh the user token
refreshUserToken().then(resolve("User token refreshed"), reject("Error refreshing user token"))
}
resolve("User token is already valid")
})
}
// This function aims to get a user token with authentication
async requestAuthentication() {
let clientId = this.clientId
return new Promise((resolve, reject) => {
//Generate a code verifier
const codeVerifier = randomstring.generate(128);
const codechallenge = base64url.fromBase64(crypto.createHash("sha256").update(codeVerifier).digest("base64"))
// Create the authentication URL
let args = new URLSearchParams({
response_type: 'code',
client_id: clientId,
scope: configuration.AuthorizationScopes.join(' '),
redirect_uri: "http://localhost:3333/callback",
code_challenge_method: 'S256',
code_challenge: codechallenge
});
// Open the authentication page
opn('https://accounts.spotify.com/authorize?' + args);
// Create an express app
let app = express()
// Catch the response
app.get('/callback', async (req, res) => {
if(req.query.code != undefined) {
const params = new URLSearchParams();
params.append("client_id", this.clientId);
params.append("grant_type", "authorization_code");
params.append("code", req.query.code);
params.append("redirect_uri", "http://localhost:3333/callback");
params.append("code_verifier", codeVerifier);
executePostRequest(configuration.SpotifyEndpoints.GET_USER_TOKEN_ENDPOINT, params, { headers: { "Content-Type": "application/x-www-form-urlencoded" } }, "json").then(result => {
console.log(result)
}, error => {
console.log(error)
})
} else {
res.send("Spotify authentication failed! Please close this tab and try again.")
reject("Error when executing request: no code provided.")
}
});
app.use(function(req, res, next) {
res.status(404)
res.send('404: File Not Found')
});
app.listen(3333, function () {
console.log('Callback server is running!')
});
resolve()
}, error => {
reject(error)
})
}
// This function aims to refresh the user token
async refreshUserToken() {
return new Promise((resolve, reject) => {
resolve()
})
}
}
// Execute a GET request to the Spotify API
function executeGetRequest(url, requestObject, options, responseType){
return new Promise((resolve, reject) => {
axios.get(url, requestObject.serializeBinary(), options)
.then(function(response){
if (response.statusText !== 'OK') {
reject(`Request error (${response.status}): ${response.statusText}`)
} else {
resolve(responseType.deserializeBinary(response.data).toObject());
}
})
.catch(function(error){
reject(error)
})
})
}
// Execute a POST request to the Spotify API
function executePostRequest(url, requestObject, options, responseType){
return new Promise((resolve, reject) => {
axios.post(url, requestObject, options)
.then(function(response){
if (response.statusText !== 'OK') {
reject(`Request error (${response.status}): ${response.statusText}`)
} else {
resolve(response.data);
}
})
.catch(function(error){
reject(error)
})
})
}
module.exports = SpotifyController;