Hacky, but working spotify skipping.

This commit is contained in:
Ian Wijma 2023-11-05 00:08:54 +11:00
parent fecd3db3bc
commit 64f1d76778
6 changed files with 221 additions and 38 deletions

116
package-lock.json generated
View File

@ -13,6 +13,7 @@
"express": "^4.18.2",
"nanoid": "^3.3.6",
"pkg": "^5.8.1",
"spotify-web-api-node": "^5.0.2",
"yargs": "^17.7.2"
},
"bin": {
@ -434,6 +435,11 @@
"node": ">=8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@ -648,6 +654,22 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -699,6 +721,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -755,6 +782,14 @@
"node": ">= 0.4"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -955,6 +990,11 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"node_modules/fastq": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
@ -1018,6 +1058,28 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formidable": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
"integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",
"deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau",
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -2143,6 +2205,14 @@
"node": ">=8"
}
},
"node_modules/spotify-web-api-node": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/spotify-web-api-node/-/spotify-web-api-node-5.0.2.tgz",
"integrity": "sha512-r82dRWU9PMimHvHEzL0DwEJrzFk+SMCVfq249SLt3I7EFez7R+jeoKQd+M1//QcnjqlXPs2am4DFsGk8/GCsrA==",
"dependencies": {
"superagent": "^6.1.0"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -2199,6 +2269,52 @@
"node": ">=0.10.0"
}
},
"node_modules/superagent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz",
"integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==",
"deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at <https://github.com/visionmedia/superagent/releases>.",
"dependencies": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.2",
"debug": "^4.1.1",
"fast-safe-stringify": "^2.0.7",
"form-data": "^3.0.0",
"formidable": "^1.2.2",
"methods": "^1.1.2",
"mime": "^2.4.6",
"qs": "^6.9.4",
"readable-stream": "^3.6.0",
"semver": "^7.3.2"
},
"engines": {
"node": ">= 7.0.0"
}
},
"node_modules/superagent/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/superagent/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@ -23,6 +23,7 @@
"express": "^4.18.2",
"nanoid": "^3.3.6",
"pkg": "^5.8.1",
"spotify-web-api-node": "^5.0.2",
"yargs": "^17.7.2"
},
"devDependencies": {

View File

@ -3,31 +3,36 @@ const {input} = require("@inquirer/prompts");
const querystring = require("querystring");
const {nanoid} = require("nanoid");
const express = require('express')
const {setAuth, hasAuth, authFile} = require("../utilities/authentication");
const {spotify, spotifyCredentialsFile, hasSpotifyCredentials, saveSpotifyCredentials} = require("../utilities/spotify");
exports.command = 'authenticate'
exports.description = 'starts the Spotity authentication process'
exports.builder = function (argv) {
return argv.option('client-id')
}
;
exports.builder = (argv) => argv
.option('client-id', {
description: 'The Spotify app client id',
type: 'string'
})
.option('client-secret', {
description: 'The Spotify app client secret',
type: 'string'
})
exports.handler = async function (argv) {
if (hasAuth()) {
console.log(`spotify.local is already authenticated, remove "${authFile}" is you want to log out.`);
if (hasSpotifyCredentials()) {
console.log(`spotify.local is already authenticated, remove "${spotifyCredentialsFile}" is you want to log out.`);
return;
}
const port = await findAvailablePort();
const authenticateRoute = 'authenticate';
const authenticateUrl = `http://localhost:${port}/${authenticateRoute}`;
const redirectUri = `http://localhost:${port}/${authenticateRoute}`;
const tutorial = [
'Visit https://developer.spotify.com/dashboard and click "Create app"',
'Set the "App name" as "spotify.local"',
'Set the "App description" as "My spotify.local"',
'Set the "Website" as "https://code.tmp.dev/ian/spotify.local"',
`Set the "Redirect URI" as "${authenticateUrl}"`,
`Set the "Redirect URI" as "${redirectUri}"`,
'Select under "Which API/SDKs are you planning to use?" the "Web API"',
'Sign your life away by agreeing with the "Developer Terms of Service and Design Guidelines"',
'Click on the "Save" button',
@ -36,12 +41,18 @@ exports.handler = async function (argv) {
tutorial.forEach((string, index) => console.log(`${index+1} ${string}`));
const clientId = argv.clientId ?? await input({ message: `${tutorial.length}. Paste the clientId here` })
const clientId = argv.clientId ?? await input({ message: `${tutorial.length}. Paste the client id here` })
const clientSecret = argv.clientSecret ?? await input({ message: `${tutorial.length}. Paste the client secret here` })
const originalState = nanoid();
const redirectUrl = new URL('https://accounts.spotify.com/authorize');
redirectUrl.search = querystring.stringify({
spotify.setClientId(clientId);
spotify.setClientSecret(clientSecret);
spotify.setRedirectURI(redirectUri);
const authUrl = new URL('https://accounts.spotify.com/authorize');
authUrl.search = querystring.stringify({
response_type: 'code',
client_id: clientId,
client_id: spotify.getClientId(),
scope: [
'user-library-read',
'user-read-private',
@ -59,28 +70,35 @@ exports.handler = async function (argv) {
'user-read-currently-playing',
'user-read-playback-position'
].join(' '),
redirect_uri: authenticateUrl,
redirect_uri: spotify.getRedirectURI(),
state: originalState
});
const app = express();
let server;
app.get(`/${authenticateRoute}`, (req, res) => {
const {state, code} = req.query;
if (originalState === state) {
setAuth(code);
app.get(`/${authenticateRoute}`, async (req, res) => {
const {state, code, error = null} = req.query;
if (!error && originalState === state) {
const { body } = await spotify.authorizationCodeGrant(code)
const {access_token, refresh_token, expires_in} = body;
spotify.setExpired((new Date).getTime() + expires_in);
spotify.setAccessToken(access_token);
spotify.setRefreshToken(refresh_token);
saveSpotifyCredentials();
res.send('<script>(() => close())()</script>');
server?.close();
console.log(`${tutorial.length+2}. Spotify.local has been authenticated`);
} else {
res.status(400);
res.send('NO')
return;
}
res.status(400);
res.send('NO');
});
server = app.listen(port, () => console.log(`${tutorial.length+1}. Follow the following URL to authenticate: ${redirectUrl}`));
server = app.listen(port, () => console.log(`${tutorial.length+1}. Follow the following URL to authenticate: ${authUrl}`));
}

8
src/commands/next.js Normal file
View File

@ -0,0 +1,8 @@
const { spotify} = require('../utilities/spotify')
exports.command = 'next'
exports.handler = async () => {
spotify.ensure()
.then(spotify => spotify.skipToNext())
.catch(e => console.error(e.message))
};

View File

@ -1,16 +0,0 @@
const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('fs');
const { homedir } = require('os');
const configDir = `${homedir()}/.spotify-local`
const authenticationFile = `${configDir}/authentication`
const ensure = () => {
if (!existsSync(configDir)) mkdirSync(configDir);
if (!existsSync(authenticationFile)) writeFileSync(authenticationFile, '');
return true;
};
exports.authFile = authenticationFile;
exports.setAuth = (code) => ensure() && writeFileSync(authenticationFile, code);
exports.getAuth = () => ensure() && readFileSync(authenticationFile).toString();
exports.hasAuth = () => ensure() && readFileSync(authenticationFile).toString() !== '';

56
src/utilities/spotify.js Normal file
View File

@ -0,0 +1,56 @@
const SpotifyWebApi = require('spotify-web-api-node');
const {homedir} = require("os");
const {existsSync, mkdirSync, writeFileSync, readFileSync} = require("fs");
const configDir = `${homedir()}/.spotify-local`
const spotifyCredentialsFile = `${configDir}/credentials.json`
const ensure = () => {
if (!existsSync(configDir)) mkdirSync(configDir);
if (!existsSync(spotifyCredentialsFile)) writeFileSync(spotifyCredentialsFile, '{}');
return true;
};
SpotifyWebApi.prototype.setExpired = function (expiresIn) {
const expired = (new Date).getTime() + expiresIn;
this._setCredential('expired', expired);
}
SpotifyWebApi.prototype.getExpired = function () {
return this._getCredential('expired') ?? 0;
}
/**
* @returns {Promise<SpotifyWebApi>}
*/
SpotifyWebApi.prototype.ensure = async function () {
const timeExpired = this.getExpired();
const time = (new Date).getTime();
if (time > timeExpired) {
const { body } = await this.refreshAccessToken();
const { access_token, expires_in } = body;
this.setAccessToken(access_token);
this.setExpired((new Date).getTime() + expires_in);
saveSpotifyCredentials();
}
return this;
}
const spotify = new SpotifyWebApi();
const saveSpotifyCredentials = () => ensure() && writeFileSync(spotifyCredentialsFile, JSON.stringify(spotify.getCredentials()));
const restoreSpotifyCredentials = () => ensure() && spotify.setCredentials(JSON.parse(readFileSync(spotifyCredentialsFile).toString()));
const hasSpotifyCredentials = () => existsSync(spotifyCredentialsFile);
if (existsSync(spotifyCredentialsFile)) restoreSpotifyCredentials();
module.exports = {
spotify,
spotifyCredentialsFile,
saveSpotifyCredentials,
restoreSpotifyCredentials,
hasSpotifyCredentials,
}