diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..6e3483a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,25 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+charset = utf-8
+
+indent_style = space
+indent_size = 2
+
+trim_trailing_whitespace = true
+max_line_length = 80
+
+[*.txt]
+indent_style = tab
+indent_size = 4
+
+[*.{diff,md}]
+trim_trailing_whitespace = false
diff --git a/.sequelizerc b/.sequelizerc
index df63de1..915c148 100644
--- a/.sequelizerc
+++ b/.sequelizerc
@@ -2,5 +2,5 @@ require("dotenv-flow").config();
const path = require("path");
module.exports = {
- "config": path.resolve("config", "sequelize.js")
-};
\ No newline at end of file
+ "config": path.resolve("config", "sequelize.js")
+};
diff --git a/api/axios.js b/api/axios.js
index 9e9d2b1..63e46c0 100644
--- a/api/axios.js
+++ b/api/axios.js
@@ -5,53 +5,53 @@ const { baseAPIURL, accountsAPIURL } = require("../constants");
const logger = require("../utils/logger")(module);
const authInstance = axios.create({
- baseURL: accountsAPIURL,
- timeout: 20000,
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- "Authorization": "Basic " + (Buffer.from(process.env.CLIENT_ID + ":" + process.env.CLIENT_SECRET).toString("base64"))
- },
+ baseURL: accountsAPIURL,
+ timeout: 20000,
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + (Buffer.from(process.env.CLIENT_ID + ":" + process.env.CLIENT_SECRET).toString("base64"))
+ },
});
const uncappedAxiosInstance = axios.create({
- baseURL: baseAPIURL,
- timeout: 20000,
- headers: {
- "Content-Type": "application/json"
- },
+ baseURL: baseAPIURL,
+ timeout: 20000,
+ headers: {
+ "Content-Type": "application/json"
+ },
});
const axiosInstance = rateLimit(uncappedAxiosInstance, {
- maxRequests: 10,
- perMilliseconds: 5000,
+ maxRequests: 10,
+ perMilliseconds: 5000,
});
axiosInstance.interceptors.request.use(config => {
- logger.http("API call", {
- url: config.url,
- method: config.method,
- params: config.params ?? {},
- headers: Object.keys(config.headers),
- });
- return config;
+ logger.http("API call", {
+ url: config.url,
+ method: config.method,
+ params: config.params ?? {},
+ headers: Object.keys(config.headers),
+ });
+ return config;
});
axiosInstance.interceptors.response.use(
- (response) => response,
- (error) => {
- logger.warn("AxiosError", {
- error: {
- name: error.name,
- code: error.code,
- message: error.message,
- },
- req: error.config,
- });
- return Promise.reject(error);
- }
+ (response) => response,
+ (error) => {
+ logger.warn("AxiosError", {
+ error: {
+ name: error.name,
+ code: error.code,
+ message: error.message,
+ },
+ req: error.config,
+ });
+ return Promise.reject(error);
+ }
);
module.exports = {
- authInstance,
- axiosInstance
+ authInstance,
+ axiosInstance
};
diff --git a/api/spotify.js b/api/spotify.js
index 8f5ef78..7a3e8cc 100644
--- a/api/spotify.js
+++ b/api/spotify.js
@@ -18,147 +18,147 @@ const logPrefix = "Spotify API: ";
* @param {boolean} inlineData true if data is to be placed inside config
*/
const singleRequest = async (req, res, method, path, config = {}, data = null, inlineData = false) => {
- let resp;
- config.headers = { ...config.headers, ...req.sessHeaders };
- try {
- if (!data || (data && inlineData)) {
- if (data)
- config.data = data ?? null;
- resp = await axiosInstance[method.toLowerCase()](path, config);
- } else
- resp = await axiosInstance[method.toLowerCase()](path, data, config);
+ let resp;
+ config.headers = { ...config.headers, ...req.sessHeaders };
+ try {
+ if (!data || (data && inlineData)) {
+ if (data)
+ config.data = data ?? null;
+ resp = await axiosInstance[method.toLowerCase()](path, config);
+ } else
+ resp = await axiosInstance[method.toLowerCase()](path, data, config);
- logger.debug(logPrefix + "Successful response received.");
- return resp;
- } catch (error) {
- if (error.response) {
- // Non 2XX response received
- let logMsg;
- if (error.response.status >= 400 && error.response.status < 600) {
- res.status(error.response.status).send(error.response.data);
- logMsg = "" + error.response.status
- }
- else {
- res.sendStatus(error.response.status);
- logMsg = "???";
- }
- logger.warn(logPrefix + logMsg, {
- response: {
- data: error.response.data,
- status: error.response.status,
- }
- });
- } else if (error.request) {
- // No response received
- res.status(504).send({ message: "No response from Spotify" });
- logger.error(logPrefix + "No response", { error });
- } else {
- // Something happened in setting up the request that triggered an Error
- res.status(500).send({ message: "Internal Server Error" });
- logger.error(logPrefix + "Request failed?", { error });
- }
+ logger.debug(logPrefix + "Successful response received.");
+ return resp;
+ } catch (error) {
+ if (error.response) {
+ // Non 2XX response received
+ let logMsg;
+ if (error.response.status >= 400 && error.response.status < 600) {
+ res.status(error.response.status).send(error.response.data);
+ logMsg = "" + error.response.status
+ }
+ else {
+ res.sendStatus(error.response.status);
+ logMsg = "???";
+ }
+ logger.warn(logPrefix + logMsg, {
+ response: {
+ data: error.response.data,
+ status: error.response.status,
+ }
+ });
+ } else if (error.request) {
+ // No response received
+ res.status(504).send({ message: "No response from Spotify" });
+ logger.error(logPrefix + "No response", { error });
+ } else {
+ // Something happened in setting up the request that triggered an Error
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error(logPrefix + "Request failed?", { error });
+ }
- return null;
- };
+ return null;
+ };
}
const getUserProfile = async (req, res) => {
- const response = await singleRequest(req, res,
- "GET", "/me",
- { headers: { Authorization: `Bearer ${req.session.accessToken}` } }
- );
- return res.headersSent ? null : response.data;
+ const response = await singleRequest(req, res,
+ "GET", "/me",
+ { headers: { Authorization: `Bearer ${req.session.accessToken}` } }
+ );
+ return res.headersSent ? null : response.data;
}
const getUserPlaylistsFirstPage = async (req, res) => {
- const response = await singleRequest(req, res,
- "GET",
- `/users/${req.session.user.id}/playlists`,
- {
- params: {
- offset: 0,
- limit: 50,
- },
- });
- return res.headersSent ? null : response.data;
+ const response = await singleRequest(req, res,
+ "GET",
+ `/users/${req.session.user.id}/playlists`,
+ {
+ params: {
+ offset: 0,
+ limit: 50,
+ },
+ });
+ return res.headersSent ? null : response.data;
}
const getUserPlaylistsNextPage = async (req, res, nextURL) => {
- const response = await singleRequest(
- req, res, "GET", nextURL);
- return res.headersSent ? null : response.data;
+ const response = await singleRequest(
+ req, res, "GET", nextURL);
+ return res.headersSent ? null : response.data;
}
const getPlaylistDetailsFirstPage = async (req, res, initialFields, playlistID) => {
- const response = await singleRequest(req, res,
- "GET",
- `/playlists/${playlistID}/`,
- {
- params: {
- fields: initialFields
- },
- });
- return res.headersSent ? null : response.data;
+ const response = await singleRequest(req, res,
+ "GET",
+ `/playlists/${playlistID}/`,
+ {
+ params: {
+ fields: initialFields
+ },
+ });
+ return res.headersSent ? null : response.data;
}
const getPlaylistDetailsNextPage = async (req, res, nextURL) => {
- const response = await singleRequest(
- req, res, "GET", nextURL);
- return res.headersSent ? null : response.data;
+ const response = await singleRequest(
+ req, res, "GET", nextURL);
+ return res.headersSent ? null : response.data;
}
const addItemsToPlaylist = async (req, res, nextBatch, playlistID) => {
- const response = await singleRequest(req, res,
- "POST",
- `/playlists/${playlistID}/tracks`,
- {},
- { uris: nextBatch }, false
- )
- return res.headersSent ? null : response.data;
+ const response = await singleRequest(req, res,
+ "POST",
+ `/playlists/${playlistID}/tracks`,
+ {},
+ { uris: nextBatch }, false
+ )
+ return res.headersSent ? null : response.data;
}
const removeItemsFromPlaylist = async (req, res, nextBatch, playlistID, snapshotID) => {
- // API doesn't document this kind of deletion via the 'positions' field
- // but see here: https://github.com/spotipy-dev/spotipy/issues/95#issuecomment-2263634801
- const response = await singleRequest(req, res,
- "DELETE",
- `/playlists/${playlistID}/tracks`,
- {},
- // axios delete method doesn't have separate arg for body so hv to put it in config
- { positions: nextBatch, snapshot_id: snapshotID }, true
- );
- return res.headersSent ? null : response.data;
+ // API doesn't document this kind of deletion via the 'positions' field
+ // but see here: https://github.com/spotipy-dev/spotipy/issues/95#issuecomment-2263634801
+ const response = await singleRequest(req, res,
+ "DELETE",
+ `/playlists/${playlistID}/tracks`,
+ {},
+ // axios delete method doesn't have separate arg for body so hv to put it in config
+ { positions: nextBatch, snapshot_id: snapshotID }, true
+ );
+ return res.headersSent ? null : response.data;
}
const checkPlaylistEditable = async (req, res, playlistID, userID) => {
- let checkFields = ["collaborative", "owner(id)"];
+ let checkFields = ["collaborative", "owner(id)"];
- const checkFromData = await getPlaylistDetailsFirstPage(req, res, checkFields.join(), playlistID);
- if (res.headersSent) return false;
+ const checkFromData = await getPlaylistDetailsFirstPage(req, res, checkFields.join(), playlistID);
+ if (res.headersSent) return false;
- // https://web.archive.org/web/20241226081630/https://developer.spotify.com/documentation/web-api/concepts/playlists#:~:text=A%20playlist%20can%20also%20be%20made%20collaborative
- // playlist is editable if it's collaborative (and thus private) or owned by the user
- if (checkFromData.collaborative !== true &&
- checkFromData.owner.id !== userID) {
- res.status(403).send({
- message: "You cannot edit this playlist, you must be the owner/the playlist must be collaborative",
- playlistID: playlistID
- });
- logger.info("user cannot edit target playlist", { playlistID: playlistID });
- return false;
- } else {
- return true;
- }
+ // https://web.archive.org/web/20241226081630/https://developer.spotify.com/documentation/web-api/concepts/playlists#:~:text=A%20playlist%20can%20also%20be%20made%20collaborative
+ // playlist is editable if it's collaborative (and thus private) or owned by the user
+ if (checkFromData.collaborative !== true &&
+ checkFromData.owner.id !== userID) {
+ res.status(403).send({
+ message: "You cannot edit this playlist, you must be the owner/the playlist must be collaborative",
+ playlistID: playlistID
+ });
+ logger.info("user cannot edit target playlist", { playlistID: playlistID });
+ return false;
+ } else {
+ return true;
+ }
}
module.exports = {
- singleRequest,
- getUserProfile,
- getUserPlaylistsFirstPage,
- getUserPlaylistsNextPage,
- getPlaylistDetailsFirstPage,
- getPlaylistDetailsNextPage,
- addItemsToPlaylist,
- removeItemsFromPlaylist,
- checkPlaylistEditable,
-}
\ No newline at end of file
+ singleRequest,
+ getUserProfile,
+ getUserPlaylistsFirstPage,
+ getUserPlaylistsNextPage,
+ getPlaylistDetailsFirstPage,
+ getPlaylistDetailsNextPage,
+ addItemsToPlaylist,
+ removeItemsFromPlaylist,
+ checkPlaylistEditable,
+}
diff --git a/boilerplates/controller.js b/boilerplates/controller.js
index 9a0b9bc..9619ed6 100644
--- a/boilerplates/controller.js
+++ b/boilerplates/controller.js
@@ -7,15 +7,15 @@ const typedefs = require("../typedefs");
* @param {typedefs.Res} res
*/
const __controller_func = async (req, res) => {
- try {
+ try {
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("__controller_func", { error });
- return;
- }
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("__controller_func", { error });
+ return;
+ }
}
module.exports = {
- __controller_func
+ __controller_func
};
diff --git a/boilerplates/validator.js b/boilerplates/validator.js
index 790eb73..75aa736 100644
--- a/boilerplates/validator.js
+++ b/boilerplates/validator.js
@@ -3,19 +3,19 @@ const { body, header, param, query } = require("express-validator");
const typedefs = require("../typedefs");
/**
- * @param {typedefs.Req} req
- * @param {typedefs.Res} res
- * @param {typedefs.Next} next
+ * @param {typedefs.Req} req
+ * @param {typedefs.Res} res
+ * @param {typedefs.Next} next
*/
const __validator_func = async (req, res, next) => {
- await body("field_name")
- .notEmpty()
- .withMessage("field_name not defined in body")
- .run(req);
+ await body("field_name")
+ .notEmpty()
+ .withMessage("field_name not defined in body")
+ .run(req);
- next();
+ next();
}
module.exports = {
- __validator_func
+ __validator_func
}
diff --git a/config/sequelize.js b/config/sequelize.js
index b07716a..903e981 100644
--- a/config/sequelize.js
+++ b/config/sequelize.js
@@ -1,28 +1,28 @@
const logger = require("../utils/logger")(module);
const connConfigs = {
- development: {
- username: process.env.DB_USER || "postgres",
- password: process.env.DB_PASSWD || "",
- database: process.env.DB_NAME || "postgres",
- host: process.env.DB_HOST || "127.0.0.1",
- port: process.env.DB_PORT || 5432,
- },
- staging: {
- use_env_variable: "DB_URL", // use connection string for non-dev env
- },
- production: {
- use_env_variable: "DB_URL", // use connection string for non-dev env
- // dialectOptions: {
- // ssl: true,
- // },
- }
+ development: {
+ username: process.env.DB_USER || "postgres",
+ password: process.env.DB_PASSWD || "",
+ database: process.env.DB_NAME || "postgres",
+ host: process.env.DB_HOST || "127.0.0.1",
+ port: process.env.DB_PORT || 5432,
+ },
+ staging: {
+ use_env_variable: "DB_URL", // use connection string for non-dev env
+ },
+ production: {
+ use_env_variable: "DB_URL", // use connection string for non-dev env
+ // dialectOptions: {
+ // ssl: true,
+ // },
+ }
}
// common config
for (const conf in connConfigs) {
- connConfigs[conf]["logging"] = (msg) => logger.debug(msg);
- connConfigs[conf]["dialect"] = process.env.DB_DIALECT || "postgres";
+ connConfigs[conf]["logging"] = (msg) => logger.debug(msg);
+ connConfigs[conf]["dialect"] = process.env.DB_DIALECT || "postgres";
}
module.exports = connConfigs;
diff --git a/constants.js b/constants.js
index aa06c72..ae6d41e 100644
--- a/constants.js
+++ b/constants.js
@@ -4,22 +4,22 @@ const sessionName = "spotify-manager";
const stateKey = "spotify_auth_state";
const scopes = {
- // ImageUpload: "ugc-image-upload",
- AccessPrivatePlaylists: "playlist-read-private",
- AccessCollaborativePlaylists: "playlist-read-collaborative",
- ModifyPublicPlaylists: "playlist-modify-public",
- ModifyPrivatePlaylists: "playlist-modify-private",
- // ModifyFollow: "user-follow-modify",
- AccessFollow: "user-follow-read",
- ModifyLibrary: "user-library-modify",
- AccessLibrary: "user-library-read",
- AccessUser: "user-read-private",
+ // ImageUpload: "ugc-image-upload",
+ AccessPrivatePlaylists: "playlist-read-private",
+ AccessCollaborativePlaylists: "playlist-read-collaborative",
+ ModifyPublicPlaylists: "playlist-modify-public",
+ ModifyPrivatePlaylists: "playlist-modify-private",
+ // ModifyFollow: "user-follow-modify",
+ AccessFollow: "user-follow-read",
+ ModifyLibrary: "user-library-modify",
+ AccessLibrary: "user-library-read",
+ AccessUser: "user-read-private",
};
module.exports = {
- accountsAPIURL,
- baseAPIURL,
- sessionName,
- stateKey,
- scopes
+ accountsAPIURL,
+ baseAPIURL,
+ sessionName,
+ stateKey,
+ scopes
};
diff --git a/controllers/auth.js b/controllers/auth.js
index 5efe4d9..3df72fd 100644
--- a/controllers/auth.js
+++ b/controllers/auth.js
@@ -13,125 +13,125 @@ const logger = require("../utils/logger")(module);
* @param {typedefs.Res} res
*/
const login = (_req, res) => {
- try {
- const state = generateRandString(16);
- res.cookie(stateKey, state);
+ try {
+ const state = generateRandString(16);
+ res.cookie(stateKey, state);
- const scope = Object.values(scopes).join(" ");
- res.redirect(
- `${accountsAPIURL}/authorize?` +
- new URLSearchParams({
- response_type: "code",
- client_id: process.env.CLIENT_ID,
- scope: scope,
- redirect_uri: process.env.REDIRECT_URI,
- state: state
- }).toString()
- );
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("login", { error });
- return;
- }
+ const scope = Object.values(scopes).join(" ");
+ res.redirect(
+ `${accountsAPIURL}/authorize?` +
+ new URLSearchParams({
+ response_type: "code",
+ client_id: process.env.CLIENT_ID,
+ scope: scope,
+ redirect_uri: process.env.REDIRECT_URI,
+ state: state
+ }).toString()
+ );
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("login", { error });
+ return;
+ }
}
/**
* Exchange authorization code for refresh and access tokens
- * @param {typedefs.Req} req
- * @param {typedefs.Res} res
+ * @param {typedefs.Req} req
+ * @param {typedefs.Res} res
*/
const callback = async (req, res) => {
- try {
- const { code, state, error } = req.query;
- const storedState = req.cookies ? req.cookies[stateKey] : null;
+ try {
+ const { code, state, error } = req.query;
+ const storedState = req.cookies ? req.cookies[stateKey] : null;
- // check state
- if (state === null || state !== storedState) {
- res.redirect(409, "/");
- logger.warn("state mismatch");
- return;
- } else if (error) {
- res.status(401).send({ message: "Auth callback error" });
- logger.error("callback error", { error });
- return;
- } else {
- // get auth tokens
- res.clearCookie(stateKey);
+ // check state
+ if (state === null || state !== storedState) {
+ res.redirect(409, "/");
+ logger.warn("state mismatch");
+ return;
+ } else if (error) {
+ res.status(401).send({ message: "Auth callback error" });
+ logger.error("callback error", { error });
+ return;
+ } else {
+ // get auth tokens
+ res.clearCookie(stateKey);
- const authForm = {
- code: code,
- redirect_uri: process.env.REDIRECT_URI,
- grant_type: "authorization_code"
- }
+ const authForm = {
+ code: code,
+ redirect_uri: process.env.REDIRECT_URI,
+ grant_type: "authorization_code"
+ }
- const authPayload = (new URLSearchParams(authForm)).toString();
+ const authPayload = (new URLSearchParams(authForm)).toString();
- const tokenResponse = await authInstance.post("/api/token", authPayload);
+ const tokenResponse = await authInstance.post("/api/token", authPayload);
- if (tokenResponse.status === 200) {
- logger.debug("Tokens obtained.");
- req.session.accessToken = tokenResponse.data.access_token;
- req.session.refreshToken = tokenResponse.data.refresh_token;
- } else {
- logger.error("login failed", { statusCode: tokenResponse.status });
- res.status(tokenResponse.status).send({ message: "Error: Login failed" });
- }
+ if (tokenResponse.status === 200) {
+ logger.debug("Tokens obtained.");
+ req.session.accessToken = tokenResponse.data.access_token;
+ req.session.refreshToken = tokenResponse.data.refresh_token;
+ } else {
+ logger.error("login failed", { statusCode: tokenResponse.status });
+ res.status(tokenResponse.status).send({ message: "Error: Login failed" });
+ }
- const userData = await getUserProfile(req, res);
- if (res.headersSent) return;
+ const userData = await getUserProfile(req, res);
+ if (res.headersSent) return;
- /** @type {typedefs.User} */
- req.session.user = {
- username: userData.display_name,
- id: userData.id,
- };
+ /** @type {typedefs.User} */
+ req.session.user = {
+ username: userData.display_name,
+ id: userData.id,
+ };
- // res.status(200).send({ message: "OK" });
- res.redirect(process.env.APP_URI + "?login=success");
- logger.debug("New login.", { username: userData.display_name });
- return;
- }
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("callback", { error });
- return;
- }
+ // res.status(200).send({ message: "OK" });
+ res.redirect(process.env.APP_URI + "?login=success");
+ logger.debug("New login.", { username: userData.display_name });
+ return;
+ }
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("callback", { error });
+ return;
+ }
}
/**
* Request new access token using refresh token
- * @param {typedefs.Req} req
+ * @param {typedefs.Req} req
* @param {typedefs.Res} res
*/
const refresh = async (req, res) => {
- try {
- const authForm = {
- refresh_token: req.session.refreshToken,
- grant_type: "refresh_token",
- }
+ try {
+ const authForm = {
+ refresh_token: req.session.refreshToken,
+ grant_type: "refresh_token",
+ }
- const authPayload = (new URLSearchParams(authForm)).toString();
+ const authPayload = (new URLSearchParams(authForm)).toString();
- const response = await authInstance.post("/api/token", authPayload);
+ const response = await authInstance.post("/api/token", authPayload);
- if (response.status === 200) {
- req.session.accessToken = response.data.access_token;
- req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; // refresh token rotation
+ if (response.status === 200) {
+ req.session.accessToken = response.data.access_token;
+ req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; // refresh token rotation
- res.status(200).send({ message: "OK" });
- logger.debug(`Access token refreshed${(response.data.refresh_token !== null) ? " and refresh token updated" : ""}.`);
- return;
- } else {
- res.status(response.status).send({ message: "Error: Refresh token flow failed." });
- logger.error("refresh failed", { statusCode: response.status });
- return;
- }
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("refresh", { error });
- return;
- }
+ res.status(200).send({ message: "OK" });
+ logger.debug(`Access token refreshed${(response.data.refresh_token !== null) ? " and refresh token updated" : ""}.`);
+ return;
+ } else {
+ res.status(response.status).send({ message: "Error: Refresh token flow failed." });
+ logger.error("refresh failed", { statusCode: response.status });
+ return;
+ }
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("refresh", { error });
+ return;
+ }
};
/**
@@ -140,30 +140,30 @@ const refresh = async (req, res) => {
* @param {typedefs.Res} res
*/
const logout = async (req, res) => {
- try {
- const delSession = req.session.destroy((error) => {
- if (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("Error while logging out", { error });
- return;
- } else {
- res.clearCookie(sessionName);
- // res.status(200).send({ message: "OK" });
- res.redirect(process.env.APP_URI + "?logout=success");
- logger.debug("Logged out.", { sessionID: delSession.id });
- return;
- }
- })
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("logout", { error });
- return;
- }
+ try {
+ const delSession = req.session.destroy((error) => {
+ if (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("Error while logging out", { error });
+ return;
+ } else {
+ res.clearCookie(sessionName);
+ // res.status(200).send({ message: "OK" });
+ res.redirect(process.env.APP_URI + "?logout=success");
+ logger.debug("Logged out.", { sessionID: delSession.id });
+ return;
+ }
+ })
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("logout", { error });
+ return;
+ }
}
module.exports = {
- login,
- callback,
- refresh,
- logout
+ login,
+ callback,
+ refresh,
+ logout
};
diff --git a/controllers/operations.js b/controllers/operations.js
index 7987c73..7f64fd8 100644
--- a/controllers/operations.js
+++ b/controllers/operations.js
@@ -19,112 +19,112 @@ const Links = require("../models").links;
* @param {typedefs.Res} res
*/
const updateUser = async (req, res) => {
- try {
- let currentPlaylists = [];
- const uID = req.session.user.id;
+ try {
+ let currentPlaylists = [];
+ const uID = req.session.user.id;
- // get first 50
- const respData = await getUserPlaylistsFirstPage(req, res);
- if (res.headersSent) return;
+ // get first 50
+ const respData = await getUserPlaylistsFirstPage(req, res);
+ if (res.headersSent) return;
- currentPlaylists = respData.items.map(playlist => {
- return {
- playlistID: playlist.id,
- playlistName: playlist.name
- }
- });
- let nextURL = respData.next;
+ currentPlaylists = respData.items.map(playlist => {
+ return {
+ playlistID: playlist.id,
+ playlistName: playlist.name
+ }
+ });
+ let nextURL = respData.next;
- // keep getting batches of 50 till exhausted
- while (nextURL) {
- const nextData = await getUserPlaylistsNextPage(req, res, nextURL);
- if (res.headersSent) return;
+ // keep getting batches of 50 till exhausted
+ while (nextURL) {
+ const nextData = await getUserPlaylistsNextPage(req, res, nextURL);
+ if (res.headersSent) return;
- currentPlaylists.push(
- ...nextData.items.map(playlist => {
- return {
- playlistID: playlist.id,
- playlistName: playlist.name
- }
- })
- );
+ currentPlaylists.push(
+ ...nextData.items.map(playlist => {
+ return {
+ playlistID: playlist.id,
+ playlistName: playlist.name
+ }
+ })
+ );
- nextURL = nextData.next;
- }
+ nextURL = nextData.next;
+ }
- let oldPlaylists = await Playlists.findAll({
- attributes: ["playlistID"],
- raw: true,
- where: {
- userID: uID
- },
- });
+ let oldPlaylists = await Playlists.findAll({
+ attributes: ["playlistID"],
+ raw: true,
+ where: {
+ userID: uID
+ },
+ });
- let toRemovePls, toAddPls;
- if (oldPlaylists.length) {
- // existing user
- const currentSet = new Set(currentPlaylists.map(pl => pl.playlistID));
- const oldSet = new Set(oldPlaylists.map(pl => pl.playlistID));
+ let toRemovePls, toAddPls;
+ if (oldPlaylists.length) {
+ // existing user
+ const currentSet = new Set(currentPlaylists.map(pl => pl.playlistID));
+ const oldSet = new Set(oldPlaylists.map(pl => pl.playlistID));
- // TODO: update playlist name
- toAddPls = currentPlaylists.filter(current => !oldSet.has(current.playlistID));
- toRemovePls = oldPlaylists.filter(old => !currentSet.has(old.playlistID));
- } else {
- // new user
- toAddPls = currentPlaylists;
- toRemovePls = [];
- }
- let toRemovePlIDs = toRemovePls.map(pl => pl.playlistID);
+ // TODO: update playlist name
+ toAddPls = currentPlaylists.filter(current => !oldSet.has(current.playlistID));
+ toRemovePls = oldPlaylists.filter(old => !currentSet.has(old.playlistID));
+ } else {
+ // new user
+ toAddPls = currentPlaylists;
+ toRemovePls = [];
+ }
+ let toRemovePlIDs = toRemovePls.map(pl => pl.playlistID);
- let removedLinks = 0, cleanedUser = 0, updatedUser = [];
+ let removedLinks = 0, cleanedUser = 0, updatedUser = [];
- if (toRemovePls.length) {
- // clean up any links dependent on the playlists
- removedLinks = await Links.destroy({
- where: {
- [Op.and]: [
- { userID: uID },
- {
- [Op.or]: [
- { from: { [Op.in]: toRemovePlIDs } },
- { to: { [Op.in]: toRemovePlIDs } },
- ]
- }
- ]
- }
- })
+ if (toRemovePls.length) {
+ // clean up any links dependent on the playlists
+ removedLinks = await Links.destroy({
+ where: {
+ [Op.and]: [
+ { userID: uID },
+ {
+ [Op.or]: [
+ { from: { [Op.in]: toRemovePlIDs } },
+ { to: { [Op.in]: toRemovePlIDs } },
+ ]
+ }
+ ]
+ }
+ })
- // only then remove
- cleanedUser = await Playlists.destroy({
- where: { playlistID: toRemovePlIDs }
- });
- if (cleanedUser !== toRemovePls.length) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.warn("Could not remove all old playlists", { error: new Error("Playlists.destroy failed?") });
- return;
- }
- }
+ // only then remove
+ cleanedUser = await Playlists.destroy({
+ where: { playlistID: toRemovePlIDs }
+ });
+ if (cleanedUser !== toRemovePls.length) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.warn("Could not remove all old playlists", { error: new Error("Playlists.destroy failed?") });
+ return;
+ }
+ }
- if (toAddPls.length) {
- updatedUser = await Playlists.bulkCreate(
- toAddPls.map(pl => { return { ...pl, userID: uID } }),
- { validate: true }
- );
- if (updatedUser.length !== toAddPls.length) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("Could not add all new playlists", { error: new Error("Playlists.bulkCreate failed?") });
- return;
- }
- }
+ if (toAddPls.length) {
+ updatedUser = await Playlists.bulkCreate(
+ toAddPls.map(pl => { return { ...pl, userID: uID } }),
+ { validate: true }
+ );
+ if (updatedUser.length !== toAddPls.length) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("Could not add all new playlists", { error: new Error("Playlists.bulkCreate failed?") });
+ return;
+ }
+ }
- res.status(200).send({ removedLinks: removedLinks > 0 });
- logger.debug("Updated user data", { delLinks: removedLinks, delPls: cleanedUser, addPls: updatedUser.length });
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("updateUser", { error });
- return;
- }
+ res.status(200).send({ removedLinks: removedLinks > 0 });
+ logger.debug("Updated user data", { delLinks: removedLinks, delPls: cleanedUser, addPls: updatedUser.length });
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("updateUser", { error });
+ return;
+ }
}
/**
@@ -133,40 +133,40 @@ const updateUser = async (req, res) => {
* @param {typedefs.Res} res
*/
const fetchUser = async (req, res) => {
- try {
- // if (randomBool(0.5)) {
- // res.status(404).send({ message: "Not Found" });
- // return;
- // }
- const uID = req.session.user.id;
+ try {
+ // if (randomBool(0.5)) {
+ // res.status(404).send({ message: "Not Found" });
+ // return;
+ // }
+ const uID = req.session.user.id;
- const currentPlaylists = await Playlists.findAll({
- attributes: ["playlistID", "playlistName"],
- raw: true,
- where: {
- userID: uID
- },
- });
+ const currentPlaylists = await Playlists.findAll({
+ attributes: ["playlistID", "playlistName"],
+ raw: true,
+ where: {
+ userID: uID
+ },
+ });
- const currentLinks = await Links.findAll({
- attributes: ["from", "to"],
- raw: true,
- where: {
- userID: uID
- },
- });
+ const currentLinks = await Links.findAll({
+ attributes: ["from", "to"],
+ raw: true,
+ where: {
+ userID: uID
+ },
+ });
- res.status(200).send({
- playlists: currentPlaylists,
- links: currentLinks
- });
- logger.debug("Fetched user data", { pls: currentPlaylists.length, links: currentLinks.length });
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("fetchUser", { error });
- return;
- }
+ res.status(200).send({
+ playlists: currentPlaylists,
+ links: currentLinks
+ });
+ logger.debug("Fetched user data", { pls: currentPlaylists.length, links: currentLinks.length });
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("fetchUser", { error });
+ return;
+ }
}
/**
@@ -175,88 +175,88 @@ const fetchUser = async (req, res) => {
* @param {typedefs.Res} res
*/
const createLink = async (req, res) => {
- try {
- // await sleep(1000);
- const uID = req.session.user.id;
+ try {
+ // await sleep(1000);
+ const uID = req.session.user.id;
- let fromPl, toPl;
- try {
- fromPl = parseSpotifyLink(req.body.from);
- toPl = parseSpotifyLink(req.body.to);
- if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
- res.status(400).send({ message: "Link is not a playlist" });
- logger.info("non-playlist link provided", { from: fromPl, to: toPl });
- return;
- }
- } catch (error) {
- res.status(400).send({ message: "Could not parse link" });
- logger.warn("parseSpotifyLink", { error });
- return;
- }
+ let fromPl, toPl;
+ try {
+ fromPl = parseSpotifyLink(req.body.from);
+ toPl = parseSpotifyLink(req.body.to);
+ if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
+ res.status(400).send({ message: "Link is not a playlist" });
+ logger.info("non-playlist link provided", { from: fromPl, to: toPl });
+ return;
+ }
+ } catch (error) {
+ res.status(400).send({ message: "Could not parse link" });
+ logger.warn("parseSpotifyLink", { error });
+ return;
+ }
- let playlists = await Playlists.findAll({
- attributes: ["playlistID"],
- raw: true,
- where: { userID: uID }
- });
- playlists = playlists.map(pl => pl.playlistID);
+ let playlists = await Playlists.findAll({
+ attributes: ["playlistID"],
+ raw: true,
+ where: { userID: uID }
+ });
+ playlists = playlists.map(pl => pl.playlistID);
- // if playlists are unknown
- if (![fromPl, toPl].every(pl => playlists.includes(pl.id))) {
- res.status(404).send({ message: "Playlists out of sync " });
- logger.warn("unknown playlists, resync");
- return;
- }
+ // if playlists are unknown
+ if (![fromPl, toPl].every(pl => playlists.includes(pl.id))) {
+ res.status(404).send({ message: "Playlists out of sync " });
+ logger.warn("unknown playlists, resync");
+ return;
+ }
- // check if exists
- const existingLink = await Links.findOne({
- where: {
- [Op.and]: [
- { userID: uID },
- { from: fromPl.id },
- { to: toPl.id }
- ]
- }
- });
- if (existingLink) {
- res.status(409).send({ message: "Link already exists!" });
- logger.info("link already exists");
- return;
- }
+ // check if exists
+ const existingLink = await Links.findOne({
+ where: {
+ [Op.and]: [
+ { userID: uID },
+ { from: fromPl.id },
+ { to: toPl.id }
+ ]
+ }
+ });
+ if (existingLink) {
+ res.status(409).send({ message: "Link already exists!" });
+ logger.info("link already exists");
+ return;
+ }
- const allLinks = await Links.findAll({
- attributes: ["from", "to"],
- raw: true,
- where: { userID: uID }
- });
+ const allLinks = await Links.findAll({
+ attributes: ["from", "to"],
+ raw: true,
+ where: { userID: uID }
+ });
- const newGraph = new myGraph(playlists, [...allLinks, { from: fromPl.id, to: toPl.id }]);
+ const newGraph = new myGraph(playlists, [...allLinks, { from: fromPl.id, to: toPl.id }]);
- if (newGraph.detectCycle()) {
- res.status(400).send({ message: "Proposed link cannot cause a cycle in the graph" });
- logger.warn("potential cycle detected");
- return;
- }
+ if (newGraph.detectCycle()) {
+ res.status(400).send({ message: "Proposed link cannot cause a cycle in the graph" });
+ logger.warn("potential cycle detected");
+ return;
+ }
- const newLink = await Links.create({
- userID: uID,
- from: fromPl.id,
- to: toPl.id
- });
- if (!newLink) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("Could not create link", { error: new Error("Links.create failed?") });
- return;
- }
+ const newLink = await Links.create({
+ userID: uID,
+ from: fromPl.id,
+ to: toPl.id
+ });
+ if (!newLink) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("Could not create link", { error: new Error("Links.create failed?") });
+ return;
+ }
- res.status(201).send({ message: "Created link." });
- logger.debug("Created link");
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("createLink", { error });
- return;
- }
+ res.status(201).send({ message: "Created link." });
+ logger.debug("Created link");
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("createLink", { error });
+ return;
+ }
}
@@ -266,399 +266,399 @@ const createLink = async (req, res) => {
* @param {typedefs.Res} res
*/
const removeLink = async (req, res) => {
- try {
- const uID = req.session.user.id;
+ try {
+ const uID = req.session.user.id;
- let fromPl, toPl;
- try {
- fromPl = parseSpotifyLink(req.body.from);
- toPl = parseSpotifyLink(req.body.to);
- if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
- res.status(400).send({ message: "Link is not a playlist" });
- logger.info("non-playlist link provided", { from: fromPl, to: toPl });
- return;
- }
- } catch (error) {
- res.status(400).send({ message: "Could not parse link" });
- logger.warn("parseSpotifyLink", { error });
- return;
- }
+ let fromPl, toPl;
+ try {
+ fromPl = parseSpotifyLink(req.body.from);
+ toPl = parseSpotifyLink(req.body.to);
+ if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
+ res.status(400).send({ message: "Link is not a playlist" });
+ logger.info("non-playlist link provided", { from: fromPl, to: toPl });
+ return;
+ }
+ } catch (error) {
+ res.status(400).send({ message: "Could not parse link" });
+ logger.warn("parseSpotifyLink", { error });
+ return;
+ }
- // check if exists
- const existingLink = await Links.findOne({
- where: {
- [Op.and]: [
- { userID: uID },
- { from: fromPl.id },
- { to: toPl.id }
- ]
- }
- });
- if (!existingLink) {
- res.status(409).send({ message: "Link does not exist!" });
- logger.warn("link does not exist");
- return;
- }
+ // check if exists
+ const existingLink = await Links.findOne({
+ where: {
+ [Op.and]: [
+ { userID: uID },
+ { from: fromPl.id },
+ { to: toPl.id }
+ ]
+ }
+ });
+ if (!existingLink) {
+ res.status(409).send({ message: "Link does not exist!" });
+ logger.warn("link does not exist");
+ return;
+ }
- const removedLink = await Links.destroy({
- where: {
- [Op.and]: [
- { userID: uID },
- { from: fromPl.id },
- { to: toPl.id }
- ]
- }
- });
- if (!removedLink) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("Could not remove link", { error: new Error("Links.destroy failed?") });
- return;
- }
+ const removedLink = await Links.destroy({
+ where: {
+ [Op.and]: [
+ { userID: uID },
+ { from: fromPl.id },
+ { to: toPl.id }
+ ]
+ }
+ });
+ if (!removedLink) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("Could not remove link", { error: new Error("Links.destroy failed?") });
+ return;
+ }
- res.status(200).send({ message: "Deleted link." });
- logger.debug("Deleted link");
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("removeLink", { error });
- return;
- }
+ res.status(200).send({ message: "Deleted link." });
+ logger.debug("Deleted link");
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("removeLink", { error });
+ return;
+ }
}
/**
* Add tracks to the link-head playlist,
* that are present in the link-tail playlist but not in the link-head playlist,
* in the order that they are present in the link-tail playlist.
- *
+ *
* eg.
- *
+ *
* pl_a has tracks: a, b, c
- *
+ *
* pl_b has tracks: e, b, d
- *
+ *
* link from pl_a to pl_b exists
- *
+ *
* after populateMissingInLink, pl_a will have tracks: a, b, c, e, d
- *
+ *
* CANNOT populate local files; Spotify API does not support it yet.
- *
+ *
* @param {typedefs.Req} req
* @param {typedefs.Res} res
*/
const populateSingleLink = async (req, res) => {
- try {
- let fromPl, toPl;
- const link = { from: req.body.from, to: req.body.to };
- const uID = req.session.user.id;
+ try {
+ let fromPl, toPl;
+ const link = { from: req.body.from, to: req.body.to };
+ const uID = req.session.user.id;
- try {
- fromPl = parseSpotifyLink(link.from);
- toPl = parseSpotifyLink(link.to);
- if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
- res.status(400).send({ message: "Link is not a playlist" });
- logger.info("non-playlist link provided", link);
- return;
- }
- } catch (error) {
- res.status(400).send({ message: "Could not parse link" });
- logger.warn("parseSpotifyLink", { error });
- return;
- }
+ try {
+ fromPl = parseSpotifyLink(link.from);
+ toPl = parseSpotifyLink(link.to);
+ if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
+ res.status(400).send({ message: "Link is not a playlist" });
+ logger.info("non-playlist link provided", link);
+ return;
+ }
+ } catch (error) {
+ res.status(400).send({ message: "Could not parse link" });
+ logger.warn("parseSpotifyLink", { error });
+ return;
+ }
- // check if exists
- const existingLink = await Links.findOne({
- where: {
- [Op.and]: [
- { userID: uID },
- { from: fromPl.id },
- { to: toPl.id }
- ]
- }
- });
- if (!existingLink) {
- res.status(409).send({ message: "Link does not exist!" });
- logger.warn("link does not exist", { link });
- return;
- }
+ // check if exists
+ const existingLink = await Links.findOne({
+ where: {
+ [Op.and]: [
+ { userID: uID },
+ { from: fromPl.id },
+ { to: toPl.id }
+ ]
+ }
+ });
+ if (!existingLink) {
+ res.status(409).send({ message: "Link does not exist!" });
+ logger.warn("link does not exist", { link });
+ return;
+ }
- if (!await checkPlaylistEditable(req, res, fromPl.id, uID))
- return;
+ if (!await checkPlaylistEditable(req, res, fromPl.id, uID))
+ return;
- let initialFields = ["tracks(next,items(is_local,track(uri)))"];
- let mainFields = ["next", "items(is_local,track(uri))"];
+ let initialFields = ["tracks(next,items(is_local,track(uri)))"];
+ let mainFields = ["next", "items(is_local,track(uri))"];
- const fromData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), fromPl.id);
- if (res.headersSent) return;
+ const fromData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), fromPl.id);
+ if (res.headersSent) return;
- let fromPlaylist = {};
- // varying fields again smh
- if (fromData.tracks.next) {
- fromPlaylist.next = new URL(fromData.tracks.next);
- fromPlaylist.next.searchParams.set("fields", mainFields.join());
- fromPlaylist.next = fromPlaylist.next.href;
- }
- fromPlaylist.tracks = fromData.tracks.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- });
+ let fromPlaylist = {};
+ // varying fields again smh
+ if (fromData.tracks.next) {
+ fromPlaylist.next = new URL(fromData.tracks.next);
+ fromPlaylist.next.searchParams.set("fields", mainFields.join());
+ fromPlaylist.next = fromPlaylist.next.href;
+ }
+ fromPlaylist.tracks = fromData.tracks.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ });
- // keep getting batches of 50 till exhausted
- while (fromPlaylist.next) {
- const nextData = await getPlaylistDetailsNextPage(req, res, fromPlaylist.next);
- if (res.headersSent) return;
+ // keep getting batches of 50 till exhausted
+ while (fromPlaylist.next) {
+ const nextData = await getPlaylistDetailsNextPage(req, res, fromPlaylist.next);
+ if (res.headersSent) return;
- fromPlaylist.tracks.push(
- ...nextData.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- })
- );
+ fromPlaylist.tracks.push(
+ ...nextData.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ })
+ );
- fromPlaylist.next = nextData.next;
- }
+ fromPlaylist.next = nextData.next;
+ }
- delete fromPlaylist.next;
+ delete fromPlaylist.next;
- const toData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), toPl.id);
- if (res.headersSent) return;
+ const toData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), toPl.id);
+ if (res.headersSent) return;
- let toPlaylist = {};
- // varying fields again smh
- if (toData.tracks.next) {
- toPlaylist.next = new URL(toData.tracks.next);
- toPlaylist.next.searchParams.set("fields", mainFields.join());
- toPlaylist.next = toPlaylist.next.href;
- }
- toPlaylist.tracks = toData.tracks.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- });
+ let toPlaylist = {};
+ // varying fields again smh
+ if (toData.tracks.next) {
+ toPlaylist.next = new URL(toData.tracks.next);
+ toPlaylist.next.searchParams.set("fields", mainFields.join());
+ toPlaylist.next = toPlaylist.next.href;
+ }
+ toPlaylist.tracks = toData.tracks.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ });
- // keep getting batches of 50 till exhausted
- while (toPlaylist.next) {
- const nextData = await getPlaylistDetailsNextPage(req, res, toPlaylist.next);
- if (res.headersSent) return;
+ // keep getting batches of 50 till exhausted
+ while (toPlaylist.next) {
+ const nextData = await getPlaylistDetailsNextPage(req, res, toPlaylist.next);
+ if (res.headersSent) return;
- toPlaylist.tracks.push(
- ...nextData.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- })
- );
+ toPlaylist.tracks.push(
+ ...nextData.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ })
+ );
- toPlaylist.next = nextData.next;
- }
+ toPlaylist.next = nextData.next;
+ }
- delete toPlaylist.next;
+ delete toPlaylist.next;
- const fromTrackURIs = fromPlaylist.tracks.map(track => track.uri);
- let toTrackURIs = toPlaylist.tracks.
- filter(track => !track.is_local). // API doesn't support adding local files to playlists yet
- filter(track => !fromTrackURIs.includes(track.uri)). // only ones missing from the 'from' playlist
- map(track => track.uri);
+ const fromTrackURIs = fromPlaylist.tracks.map(track => track.uri);
+ let toTrackURIs = toPlaylist.tracks.
+ filter(track => !track.is_local). // API doesn't support adding local files to playlists yet
+ filter(track => !fromTrackURIs.includes(track.uri)). // only ones missing from the 'from' playlist
+ map(track => track.uri);
- const toAddNum = toTrackURIs.length;
- const localNum = toPlaylist.tracks.filter(track => track.is_local).length;
+ const toAddNum = toTrackURIs.length;
+ const localNum = toPlaylist.tracks.filter(track => track.is_local).length;
- // append to end in batches of 100
- while (toTrackURIs.length > 0) {
- const nextBatch = toTrackURIs.splice(0, 100);
- const addData = await addItemsToPlaylist(req, res, nextBatch, fromPl.id);
- if (res.headersSent) return;
- }
+ // append to end in batches of 100
+ while (toTrackURIs.length > 0) {
+ const nextBatch = toTrackURIs.splice(0, 100);
+ const addData = await addItemsToPlaylist(req, res, nextBatch, fromPl.id);
+ if (res.headersSent) return;
+ }
- let logMsg;
- logMsg = toAddNum > 0 ? "Added " + toAddNum + " tracks" : "No tracks to add";
- logMsg += localNum > 0 ? ", but could not add " + localNum + " local files" : ".";
+ let logMsg;
+ logMsg = toAddNum > 0 ? "Added " + toAddNum + " tracks" : "No tracks to add";
+ logMsg += localNum > 0 ? ", but could not add " + localNum + " local files" : ".";
- res.status(200).send({ message: logMsg });
- logger.debug(logMsg);
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("populateSingleLink", { error });
- return;
- }
+ res.status(200).send({ message: logMsg });
+ logger.debug(logMsg);
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("populateSingleLink", { error });
+ return;
+ }
}
/**
* Remove tracks from the link-tail playlist,
* that are present in the link-tail playlist but not in the link-head playlist.
- *
+ *
* eg.
- *
+ *
* pl_a has tracks: a, b, c
- *
+ *
* pl_b has tracks: e, b, d, c, f, g
- *
+ *
* link from pl_a to pl_b exists
- *
+ *
* after pruneSingleLink, pl_b will have tracks: b, c
- *
+ *
* @param {typedefs.Req} req
* @param {typedefs.Res} res
*/
const pruneSingleLink = async (req, res) => {
- try {
- const uID = req.session.user.id;
- const link = { from: req.body.from, to: req.body.to };
+ try {
+ const uID = req.session.user.id;
+ const link = { from: req.body.from, to: req.body.to };
- let fromPl, toPl;
- try {
- fromPl = parseSpotifyLink(link.from);
- toPl = parseSpotifyLink(link.to);
- if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
- res.status(400).send({ message: "Link is not a playlist" });
- logger.info("non-playlist link provided", link);
- return;
- }
- } catch (error) {
- res.status(400).send({ message: error.message });
- logger.warn("parseSpotifyLink", { error });
- return;
- }
+ let fromPl, toPl;
+ try {
+ fromPl = parseSpotifyLink(link.from);
+ toPl = parseSpotifyLink(link.to);
+ if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
+ res.status(400).send({ message: "Link is not a playlist" });
+ logger.info("non-playlist link provided", link);
+ return;
+ }
+ } catch (error) {
+ res.status(400).send({ message: error.message });
+ logger.warn("parseSpotifyLink", { error });
+ return;
+ }
- // check if exists
- const existingLink = await Links.findOne({
- where: {
- [Op.and]: [
- { userID: uID },
- { from: fromPl.id },
- { to: toPl.id }
- ]
- }
- });
- if (!existingLink) {
- res.status(409).send({ message: "Link does not exist!" });
- logger.warn("link does not exist", { link });
- return;
- }
+ // check if exists
+ const existingLink = await Links.findOne({
+ where: {
+ [Op.and]: [
+ { userID: uID },
+ { from: fromPl.id },
+ { to: toPl.id }
+ ]
+ }
+ });
+ if (!existingLink) {
+ res.status(409).send({ message: "Link does not exist!" });
+ logger.warn("link does not exist", { link });
+ return;
+ }
- if (!await checkPlaylistEditable(req, res, toPl.id, uID))
- return;
+ if (!await checkPlaylistEditable(req, res, toPl.id, uID))
+ return;
- let initialFields = ["snapshot_id", "tracks(next,items(is_local,track(uri)))"];
- let mainFields = ["next", "items(is_local,track(uri))"];
+ let initialFields = ["snapshot_id", "tracks(next,items(is_local,track(uri)))"];
+ let mainFields = ["next", "items(is_local,track(uri))"];
- const fromData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), fromPl.id);
- if (res.headersSent) return;
+ const fromData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), fromPl.id);
+ if (res.headersSent) return;
- let fromPlaylist = {};
- // varying fields again smh
- fromPlaylist.snapshot_id = fromData.snapshot_id;
- if (fromData.tracks.next) {
- fromPlaylist.next = new URL(fromData.tracks.next);
- fromPlaylist.next.searchParams.set("fields", mainFields.join());
- fromPlaylist.next = fromPlaylist.next.href;
- }
- fromPlaylist.tracks = fromData.tracks.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- });
+ let fromPlaylist = {};
+ // varying fields again smh
+ fromPlaylist.snapshot_id = fromData.snapshot_id;
+ if (fromData.tracks.next) {
+ fromPlaylist.next = new URL(fromData.tracks.next);
+ fromPlaylist.next.searchParams.set("fields", mainFields.join());
+ fromPlaylist.next = fromPlaylist.next.href;
+ }
+ fromPlaylist.tracks = fromData.tracks.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ });
- // keep getting batches of 50 till exhausted
- while (fromPlaylist.next) {
- const nextData = await getPlaylistDetailsNextPage(req, res, fromPlaylist.next);
- if (res.headersSent) return;
+ // keep getting batches of 50 till exhausted
+ while (fromPlaylist.next) {
+ const nextData = await getPlaylistDetailsNextPage(req, res, fromPlaylist.next);
+ if (res.headersSent) return;
- fromPlaylist.tracks.push(
- ...nextData.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- })
- );
+ fromPlaylist.tracks.push(
+ ...nextData.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ })
+ );
- fromPlaylist.next = nextData.next;
- }
+ fromPlaylist.next = nextData.next;
+ }
- delete fromPlaylist.next;
+ delete fromPlaylist.next;
- const toData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), toPl.id);
- if (res.headersSent) return;
+ const toData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), toPl.id);
+ if (res.headersSent) return;
- let toPlaylist = {};
- // varying fields again smh
- toPlaylist.snapshot_id = toData.snapshot_id;
- if (toData.tracks.next) {
- toPlaylist.next = new URL(toData.tracks.next);
- toPlaylist.next.searchParams.set("fields", mainFields.join());
- toPlaylist.next = toPlaylist.next.href;
- }
- toPlaylist.tracks = toData.tracks.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- });
+ let toPlaylist = {};
+ // varying fields again smh
+ toPlaylist.snapshot_id = toData.snapshot_id;
+ if (toData.tracks.next) {
+ toPlaylist.next = new URL(toData.tracks.next);
+ toPlaylist.next.searchParams.set("fields", mainFields.join());
+ toPlaylist.next = toPlaylist.next.href;
+ }
+ toPlaylist.tracks = toData.tracks.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ });
- // keep getting batches of 50 till exhausted
- while (toPlaylist.next) {
- const nextData = await getPlaylistDetailsNextPage(req, res, toPlaylist.next);
- if (res.headersSent) return;
+ // keep getting batches of 50 till exhausted
+ while (toPlaylist.next) {
+ const nextData = await getPlaylistDetailsNextPage(req, res, toPlaylist.next);
+ if (res.headersSent) return;
- toPlaylist.tracks.push(
- ...nextData.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- uri: playlist_item.track.uri
- }
- })
- );
+ toPlaylist.tracks.push(
+ ...nextData.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ uri: playlist_item.track.uri
+ }
+ })
+ );
- toPlaylist.next = nextData.next;
- }
+ toPlaylist.next = nextData.next;
+ }
- delete toPlaylist.next;
+ delete toPlaylist.next;
- const fromTrackURIs = fromPlaylist.tracks.map(track => track.uri);
- let indexedToTrackURIs = toPlaylist.tracks;
+ const fromTrackURIs = fromPlaylist.tracks.map(track => track.uri);
+ let indexedToTrackURIs = toPlaylist.tracks;
- indexedToTrackURIs.forEach((track, index) => {
- track.position = index;
- });
+ indexedToTrackURIs.forEach((track, index) => {
+ track.position = index;
+ });
- let indexes = indexedToTrackURIs.filter(track => !fromTrackURIs.includes(track.uri)); // only those missing from the 'from' playlist
- indexes = indexes.map(track => track.position); // get track positions
+ let indexes = indexedToTrackURIs.filter(track => !fromTrackURIs.includes(track.uri)); // only those missing from the 'from' playlist
+ indexes = indexes.map(track => track.position); // get track positions
- const toDelNum = indexes.length;
+ const toDelNum = indexes.length;
- // remove in batches of 100 (from reverse, to preserve positions while modifying)
- let currentSnapshot = toPlaylist.snapshot_id;
- while (indexes.length) {
- const nextBatch = indexes.splice(Math.max(indexes.length - 100, 0), 100);
- const delResponse = await removeItemsFromPlaylist(req, res, nextBatch, toPl.id, currentSnapshot);
- if (res.headersSent) return;
- currentSnapshot = delResponse.snapshot_id;
- }
+ // remove in batches of 100 (from reverse, to preserve positions while modifying)
+ let currentSnapshot = toPlaylist.snapshot_id;
+ while (indexes.length) {
+ const nextBatch = indexes.splice(Math.max(indexes.length - 100, 0), 100);
+ const delResponse = await removeItemsFromPlaylist(req, res, nextBatch, toPl.id, currentSnapshot);
+ if (res.headersSent) return;
+ currentSnapshot = delResponse.snapshot_id;
+ }
- res.status(200).send({ message: `Removed ${toDelNum} tracks.` });
- logger.debug(`Pruned ${toDelNum} tracks`);
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("pruneSingleLink", { error });
- return;
- }
+ res.status(200).send({ message: `Removed ${toDelNum} tracks.` });
+ logger.debug(`Pruned ${toDelNum} tracks`);
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("pruneSingleLink", { error });
+ return;
+ }
}
module.exports = {
- updateUser,
- fetchUser,
- createLink,
- removeLink,
- populateSingleLink,
- pruneSingleLink,
+ updateUser,
+ fetchUser,
+ createLink,
+ removeLink,
+ populateSingleLink,
+ pruneSingleLink,
};
diff --git a/controllers/playlists.js b/controllers/playlists.js
index 96a5606..652176c 100644
--- a/controllers/playlists.js
+++ b/controllers/playlists.js
@@ -10,150 +10,150 @@ const { parseSpotifyLink } = require("../utils/spotifyURITransformer");
* @param {typedefs.Res} res
*/
const fetchUserPlaylists = async (req, res) => {
- try {
- let userPlaylists = {};
+ try {
+ let userPlaylists = {};
- // get first 50
- const respData = await getUserPlaylistsFirstPage(req, res);
- if (res.headersSent) return;
+ // get first 50
+ const respData = await getUserPlaylistsFirstPage(req, res);
+ if (res.headersSent) return;
- userPlaylists.total = respData.total;
+ userPlaylists.total = respData.total;
- userPlaylists.items = respData.items.map((playlist) => {
- return {
- uri: playlist.uri,
- images: playlist.images,
- name: playlist.name,
- total: playlist.tracks.total
- }
- });
+ userPlaylists.items = respData.items.map((playlist) => {
+ return {
+ uri: playlist.uri,
+ images: playlist.images,
+ name: playlist.name,
+ total: playlist.tracks.total
+ }
+ });
- userPlaylists.next = respData.next;
- // keep getting batches of 50 till exhausted
- while (userPlaylists.next) {
- const nextData = await getUserPlaylistsNextPage(req, res, userPlaylists.next);
- if (res.headersSent) return;
+ userPlaylists.next = respData.next;
+ // keep getting batches of 50 till exhausted
+ while (userPlaylists.next) {
+ const nextData = await getUserPlaylistsNextPage(req, res, userPlaylists.next);
+ if (res.headersSent) return;
- userPlaylists.items.push(
- ...nextData.items.map((playlist) => {
- return {
- uri: playlist.uri,
- images: playlist.images,
- name: playlist.name,
- total: playlist.tracks.total
- }
- })
- );
+ userPlaylists.items.push(
+ ...nextData.items.map((playlist) => {
+ return {
+ uri: playlist.uri,
+ images: playlist.images,
+ name: playlist.name,
+ total: playlist.tracks.total
+ }
+ })
+ );
- userPlaylists.next = nextData.next;
- }
+ userPlaylists.next = nextData.next;
+ }
- delete userPlaylists.next;
+ delete userPlaylists.next;
- res.status(200).send(userPlaylists);
- logger.debug("Fetched user playlists", { num: userPlaylists.total });
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("fetchUserPlaylists", { error });
- return;
- }
+ res.status(200).send(userPlaylists);
+ logger.debug("Fetched user playlists", { num: userPlaylists.total });
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("fetchUserPlaylists", { error });
+ return;
+ }
}
/**
* Retrieve an entire playlist
- * @param {typedefs.Req} req
- * @param {typedefs.Res} res
+ * @param {typedefs.Req} req
+ * @param {typedefs.Res} res
*/
const fetchPlaylistDetails = async (req, res) => {
- try {
- let playlist = {};
- /** @type {typedefs.URIObject} */
- let uri;
- let initialFields = ["collaborative", "description", "images", "name", "owner(uri,display_name)", "public",
- "snapshot_id", "tracks(next,total,items(is_local,track(name,uri)))"];
- let mainFields = ["next,items(is_local,track(name,uri))"];
+ try {
+ let playlist = {};
+ /** @type {typedefs.URIObject} */
+ let uri;
+ let initialFields = ["collaborative", "description", "images", "name", "owner(uri,display_name)", "public",
+ "snapshot_id", "tracks(next,total,items(is_local,track(name,uri)))"];
+ let mainFields = ["next,items(is_local,track(name,uri))"];
- try {
- uri = parseSpotifyLink(req.query.playlist_link)
- if (uri.type !== "playlist") {
- res.status(400).send({ message: "Link is not a playlist" });
- logger.warn("non-playlist link provided", { uri });
- return;
- }
- } catch (error) {
- res.status(400).send({ message: error.message });
- logger.warn("parseSpotifyLink", { error });
- return;
- }
+ try {
+ uri = parseSpotifyLink(req.query.playlist_link)
+ if (uri.type !== "playlist") {
+ res.status(400).send({ message: "Link is not a playlist" });
+ logger.warn("non-playlist link provided", { uri });
+ return;
+ }
+ } catch (error) {
+ res.status(400).send({ message: error.message });
+ logger.warn("parseSpotifyLink", { error });
+ return;
+ }
- const respData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), uri.id);
- if (res.headersSent) return;
+ const respData = await getPlaylistDetailsFirstPage(req, res, initialFields.join(), uri.id);
+ if (res.headersSent) return;
- // TODO: this whole section needs to be DRYer
- // look into serializr
- playlist.name = respData.name;
- playlist.description = respData.description;
- playlist.collaborative = respData.collaborative;
- playlist.public = respData.public;
- playlist.images = [...respData.images];
- playlist.owner = { ...respData.owner };
- playlist.snapshot_id = respData.snapshot_id;
- playlist.total = respData.tracks.total;
+ // TODO: this whole section needs to be DRYer
+ // look into serializr
+ playlist.name = respData.name;
+ playlist.description = respData.description;
+ playlist.collaborative = respData.collaborative;
+ playlist.public = respData.public;
+ playlist.images = [...respData.images];
+ playlist.owner = { ...respData.owner };
+ playlist.snapshot_id = respData.snapshot_id;
+ playlist.total = respData.tracks.total;
- // previous fields get carried over to the next URL, but most of these fields are not present in the new endpoint
- // API shouldn't be returning such URLs, the problem's in the API ig...
- if (respData.tracks.next) {
- playlist.next = new URL(respData.tracks.next);
- playlist.next.searchParams.set("fields", mainFields.join());
- playlist.next = playlist.next.href;
- }
- playlist.tracks = respData.tracks.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- track: {
- name: playlist_item.track.name,
- type: playlist_item.track.type,
- uri: playlist_item.track.uri
- }
- }
- });
+ // previous fields get carried over to the next URL, but most of these fields are not present in the new endpoint
+ // API shouldn't be returning such URLs, the problem's in the API ig...
+ if (respData.tracks.next) {
+ playlist.next = new URL(respData.tracks.next);
+ playlist.next.searchParams.set("fields", mainFields.join());
+ playlist.next = playlist.next.href;
+ }
+ playlist.tracks = respData.tracks.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ track: {
+ name: playlist_item.track.name,
+ type: playlist_item.track.type,
+ uri: playlist_item.track.uri
+ }
+ }
+ });
- // keep getting batches of 50 till exhausted
- while (playlist.next) {
- const nextData = await getPlaylistDetailsNextPage(req, res, playlist.next);
- if (res.headersSent) return;
+ // keep getting batches of 50 till exhausted
+ while (playlist.next) {
+ const nextData = await getPlaylistDetailsNextPage(req, res, playlist.next);
+ if (res.headersSent) return;
- playlist.tracks.push(
- ...nextData.items.map((playlist_item) => {
- return {
- is_local: playlist_item.is_local,
- track: {
- name: playlist_item.track.name,
- type: playlist_item.track.type,
- uri: playlist_item.track.uri
- }
- }
- })
- );
+ playlist.tracks.push(
+ ...nextData.items.map((playlist_item) => {
+ return {
+ is_local: playlist_item.is_local,
+ track: {
+ name: playlist_item.track.name,
+ type: playlist_item.track.type,
+ uri: playlist_item.track.uri
+ }
+ }
+ })
+ );
- playlist.next = nextData.next;
- }
+ playlist.next = nextData.next;
+ }
- delete playlist.next;
+ delete playlist.next;
- res.status(200).send(playlist);
- logger.debug("Fetched playlist tracks", { num: playlist.tracks.length });
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("getPlaylistDetails", { error });
- return;
- }
+ res.status(200).send(playlist);
+ logger.debug("Fetched playlist tracks", { num: playlist.tracks.length });
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("getPlaylistDetails", { error });
+ return;
+ }
}
module.exports = {
- fetchUserPlaylists,
- fetchPlaylistDetails
+ fetchUserPlaylists,
+ fetchPlaylistDetails
};
diff --git a/index.js b/index.js
index 158a189..2b4e77e 100644
--- a/index.js
+++ b/index.js
@@ -24,32 +24,32 @@ app.set("trust proxy", process.env.TRUST_PROXY);
// Configure SQLite store file
const sqliteStore = new SQLiteStore({
- table: "session_store",
- db: "spotify-manager.db"
+ table: "session_store",
+ db: "spotify-manager.db"
});
// Configure session middleware
app.use(session({
- name: sessionName,
- store: sqliteStore,
- secret: process.env.SESSION_SECRET,
- resave: false,
- saveUninitialized: false,
- cookie: {
- domain: process.env.BASE_DOMAIN,
- httpOnly: true, // if true prevent client side JS from reading the cookie
- maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
- sameSite: process.env.NODE_ENV === "development" ? "lax" : "none", // cross-site for production
- secure: process.env.NODE_ENV === "development" ? false : true, // if true only transmit cookie over https
- }
+ name: sessionName,
+ store: sqliteStore,
+ secret: process.env.SESSION_SECRET,
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ domain: process.env.BASE_DOMAIN,
+ httpOnly: true, // if true prevent client side JS from reading the cookie
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
+ sameSite: process.env.NODE_ENV === "development" ? "lax" : "none", // cross-site for production
+ secure: process.env.NODE_ENV === "development" ? false : true, // if true only transmit cookie over https
+ }
}));
app.use(cors({
- origin: process.env.APP_URI,
- credentials: true
+ origin: process.env.APP_URI,
+ credentials: true
}));
app.use(helmet({
- crossOriginOpenerPolicy: { policy: process.env.NODE_ENV === "development" ? "unsafe-none" : "same-origin" }
+ crossOriginOpenerPolicy: { policy: process.env.NODE_ENV === "development" ? "unsafe-none" : "same-origin" }
}));
app.disable("x-powered-by");
@@ -62,20 +62,20 @@ app.use(express.static(__dirname + "/static"));
// Healthcheck
app.use("/health", (req, res) => {
- res.status(200).send({ message: "OK" });
- return;
+ res.status(200).send({ message: "OK" });
+ return;
});
app.use("/auth-health", isAuthenticated, async (req, res) => {
- try {
- await getUserProfile(req, res);
- if (res.headersSent) return;
- res.status(200).send({ message: "OK" });
- return;
- } catch (error) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("authHealthCheck", { error });
- return;
- }
+ try {
+ await getUserProfile(req, res);
+ if (res.headersSent) return;
+ res.status(200).send({ message: "OK" });
+ return;
+ } catch (error) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("authHealthCheck", { error });
+ return;
+ }
});
// Routes
@@ -85,32 +85,32 @@ app.use("/api/operations", isAuthenticated, require("./routes/operations"));
// Fallbacks
app.use((req, res) => {
- res.status(404).send(
- "Guess the cat's out of the bag!"
- );
- logger.info("404", { url: req.url });
- return;
+ res.status(404).send(
+ "Guess the cat's out of the bag!"
+ );
+ logger.info("404", { url: req.url });
+ return;
});
const port = process.env.PORT || 5000;
const server = app.listen(port, () => {
- logger.info(`App Listening on port ${port}`);
+ logger.info(`App Listening on port ${port}`);
});
const cleanupFunc = (signal) => {
- if (signal)
- logger.debug(`${signal} signal received, shutting down now...`);
+ if (signal)
+ logger.debug(`${signal} signal received, shutting down now...`);
- Promise.allSettled([
- db.sequelize.close(),
- util.promisify(server.close),
- ]).then(() => {
- logger.info("Cleaned up, exiting.");
- process.exit(0);
- });
+ Promise.allSettled([
+ db.sequelize.close(),
+ util.promisify(server.close),
+ ]).then(() => {
+ logger.info("Cleaned up, exiting.");
+ process.exit(0);
+ });
}
["SIGHUP", "SIGINT", "SIGQUIT", "SIGTERM", "SIGUSR1", "SIGUSR2"].forEach((signal) => {
- process.on(signal, () => cleanupFunc(signal));
+ process.on(signal, () => cleanupFunc(signal));
});
diff --git a/middleware/authCheck.js b/middleware/authCheck.js
index 464d1c8..5f8941c 100644
--- a/middleware/authCheck.js
+++ b/middleware/authCheck.js
@@ -4,33 +4,33 @@ const logger = require("../utils/logger")(module);
/**
* middleware to check if access token is present
- * @param {typedefs.Req} req
- * @param {typedefs.Res} res
- * @param {typedefs.Next} next
+ * @param {typedefs.Req} req
+ * @param {typedefs.Res} res
+ * @param {typedefs.Next} next
*/
const isAuthenticated = (req, res, next) => {
- if (req.session.accessToken) {
- req.sessHeaders = {
- "Authorization": `Bearer ${req.session.accessToken}`,
- // "X-RateLimit-SessID": `${req.sessionID}_${req.session.user.username}`
- };
- next();
- } else {
- const delSession = req.session.destroy((err) => {
- if (err) {
- res.status(500).send({ message: "Internal Server Error" });
- logger.error("session.destroy", { err });
- return;
- } else {
- res.clearCookie(sessionName);
- res.status(401).send({ message: "Unauthorized" });
- logger.debug("Session invalid, destroyed.", { sessionID: delSession.id });
- return;
- }
- });
- }
+ if (req.session.accessToken) {
+ req.sessHeaders = {
+ "Authorization": `Bearer ${req.session.accessToken}`,
+ // "X-RateLimit-SessID": `${req.sessionID}_${req.session.user.username}`
+ };
+ next();
+ } else {
+ const delSession = req.session.destroy((err) => {
+ if (err) {
+ res.status(500).send({ message: "Internal Server Error" });
+ logger.error("session.destroy", { err });
+ return;
+ } else {
+ res.clearCookie(sessionName);
+ res.status(401).send({ message: "Unauthorized" });
+ logger.debug("Session invalid, destroyed.", { sessionID: delSession.id });
+ return;
+ }
+ });
+ }
}
module.exports = {
- isAuthenticated,
+ isAuthenticated,
}
diff --git a/migrations/20240727162141-create-playlists.js b/migrations/20240727162141-create-playlists.js
index 112b616..372f380 100644
--- a/migrations/20240727162141-create-playlists.js
+++ b/migrations/20240727162141-create-playlists.js
@@ -31,4 +31,4 @@ module.exports = {
async down(queryInterface, Sequelize) {
await queryInterface.dropTable("playlists");
}
-};
\ No newline at end of file
+};
diff --git a/migrations/20240730101615-create-links.js b/migrations/20240730101615-create-links.js
index 497729a..3c6823d 100644
--- a/migrations/20240730101615-create-links.js
+++ b/migrations/20240730101615-create-links.js
@@ -31,4 +31,4 @@ module.exports = {
async down(queryInterface, Sequelize) {
await queryInterface.dropTable("links");
}
-};
\ No newline at end of file
+};
diff --git a/models/index.js b/models/index.js
index 8f0da0d..cf8b61f 100644
--- a/models/index.js
+++ b/models/index.js
@@ -10,37 +10,37 @@ const db = {};
let sequelize;
if (config.use_env_variable) {
- sequelize = new Sequelize(process.env[config.use_env_variable], config);
+ sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
- sequelize = new Sequelize(config.database, config.username, config.password, config);
+ sequelize = new Sequelize(config.database, config.username, config.password, config);
}
(async () => {
- try {
- await sequelize.authenticate();
- logger.debug("Sequelize auth success");
- } catch (error) {
- logger.error("Sequelize auth error", { error });
- throw error;
- }
+ try {
+ await sequelize.authenticate();
+ logger.debug("Sequelize auth success");
+ } catch (error) {
+ logger.error("Sequelize auth error", { error });
+ throw error;
+ }
})();
// Read model definitions from folder
fs
- .readdirSync(__dirname)
- .filter(file => {
- return (file.indexOf(".") !== 0) && (file !== basename) && (file.slice(-3) === ".js");
- })
- .forEach(file => {
- const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
- db[model.name] = model;
- });
+ .readdirSync(__dirname)
+ .filter(file => {
+ return (file.indexOf(".") !== 0) && (file !== basename) && (file.slice(-3) === ".js");
+ })
+ .forEach(file => {
+ const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
+ db[model.name] = model;
+ });
// Setup defined associations
Object.keys(db).forEach(modelName => {
- if (db[modelName].associate) {
- db[modelName].associate(db);
- }
+ if (db[modelName].associate) {
+ db[modelName].associate(db);
+ }
});
db.sequelize = sequelize;
diff --git a/routes/auth.js b/routes/auth.js
index 90286bd..f3954ed 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -5,24 +5,24 @@ const { isAuthenticated } = require("../middleware/authCheck");
const validator = require("../validators");
router.get(
- "/login",
- login
+ "/login",
+ login
);
router.get(
- "/callback",
- callback
+ "/callback",
+ callback
);
router.get(
- "/refresh",
- isAuthenticated,
- refresh
+ "/refresh",
+ isAuthenticated,
+ refresh
);
router.get(
- "/logout",
- logout
+ "/logout",
+ logout
);
module.exports = router;
diff --git a/routes/operations.js b/routes/operations.js
index ff70a49..3a0efab 100644
--- a/routes/operations.js
+++ b/routes/operations.js
@@ -5,41 +5,41 @@ const { validate } = require("../validators");
const { createLinkValidator, removeLinkValidator, populateSingleLinkValidator, pruneSingleLinkValidator } = require("../validators/operations");
router.put(
- "/update",
- updateUser
+ "/update",
+ updateUser
);
router.get(
- "/fetch",
- fetchUser
+ "/fetch",
+ fetchUser
);
router.post(
- "/link",
- createLinkValidator,
- validate,
- createLink
+ "/link",
+ createLinkValidator,
+ validate,
+ createLink
);
router.delete(
- "/link",
- removeLinkValidator,
- validate,
- removeLink
+ "/link",
+ removeLinkValidator,
+ validate,
+ removeLink
);
router.put(
- "/populate/link",
- populateSingleLinkValidator,
- validate,
- populateSingleLink
+ "/populate/link",
+ populateSingleLinkValidator,
+ validate,
+ populateSingleLink
);
router.put(
- "/prune/link",
- pruneSingleLinkValidator,
- validate,
- pruneSingleLink
+ "/prune/link",
+ pruneSingleLinkValidator,
+ validate,
+ pruneSingleLink
);
module.exports = router;
diff --git a/routes/playlists.js b/routes/playlists.js
index 7ce094e..e386dea 100644
--- a/routes/playlists.js
+++ b/routes/playlists.js
@@ -5,15 +5,15 @@ const { getPlaylistDetailsValidator } = require("../validators/playlists");
const { validate } = require("../validators");
router.get(
- "/me",
- fetchUserPlaylists
+ "/me",
+ fetchUserPlaylists
);
router.get(
- "/details",
- getPlaylistDetailsValidator,
- validate,
- fetchPlaylistDetails
+ "/details",
+ getPlaylistDetailsValidator,
+ validate,
+ fetchPlaylistDetails
);
module.exports = router;
diff --git a/utils/flake.js b/utils/flake.js
index 1ff7c8e..e229209 100644
--- a/utils/flake.js
+++ b/utils/flake.js
@@ -3,5 +3,5 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const randomBool = (chance_of_failure = 0.25) => Math.random() < chance_of_failure;
module.exports = {
- sleep, randomBool
-};
\ No newline at end of file
+ sleep, randomBool
+};
diff --git a/utils/generateRandString.js b/utils/generateRandString.js
index eb31d2c..119aa17 100644
--- a/utils/generateRandString.js
+++ b/utils/generateRandString.js
@@ -4,11 +4,11 @@
* @return {string} The generated string
*/
module.exports = (length) => {
- const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- let text = "";
+ const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ let text = "";
- for (let i = 0; i < length; i++) {
- text += possible.charAt(Math.floor(Math.random() * possible.length));
- }
- return text;
+ for (let i = 0; i < length; i++) {
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
+ }
+ return text;
};
diff --git a/utils/graph.js b/utils/graph.js
index d4df430..84feb14 100644
--- a/utils/graph.js
+++ b/utils/graph.js
@@ -4,9 +4,9 @@ const typedefs = require("../typedefs");
/**
* Directed graph, may or may not be connected.
- *
+ *
* NOTE: Assumes that nodes and edges are valid.
- *
+ *
* Example:
* ```javascript
* let nodes = ["a", "b", "c", "d", "e"];
@@ -22,80 +22,80 @@ const typedefs = require("../typedefs");
* ```
*/
class myGraph {
- /**
- * @param {string[]} nodes Graph nodes IDs
- * @param {{ from: string, to: string }[]} edges Graph edges b/w nodes
- */
- constructor(nodes, edges) {
- this.nodes = [...nodes];
- this.edges = structuredClone(edges);
- }
+ /**
+ * @param {string[]} nodes Graph nodes IDs
+ * @param {{ from: string, to: string }[]} edges Graph edges b/w nodes
+ */
+ constructor(nodes, edges) {
+ this.nodes = [...nodes];
+ this.edges = structuredClone(edges);
+ }
- /**
- * @param {string} node
- * @returns {string[]}
- */
- getDirectHeads(node) {
- return this.edges.filter(edge => edge.to == node).map(edge => edge.from);
- }
+ /**
+ * @param {string} node
+ * @returns {string[]}
+ */
+ getDirectHeads(node) {
+ return this.edges.filter(edge => edge.to == node).map(edge => edge.from);
+ }
- /**
- * @param {string} node
- * @returns {string[]}
- */
- getDirectTails(node) {
- return this.edges.filter(edge => edge.from == node).map(edge => edge.to);
- }
+ /**
+ * @param {string} node
+ * @returns {string[]}
+ */
+ getDirectTails(node) {
+ return this.edges.filter(edge => edge.from == node).map(edge => edge.to);
+ }
- /**
- * Kahn's topological sort
- * @returns {string[]}
- */
- topoSort() {
- let inDegree = {};
- let zeroInDegreeQueue = [];
- let topologicalOrder = [];
+ /**
+ * Kahn's topological sort
+ * @returns {string[]}
+ */
+ topoSort() {
+ let inDegree = {};
+ let zeroInDegreeQueue = [];
+ let topologicalOrder = [];
- // Initialize inDegree of all nodes to 0
- for (let node of this.nodes) {
- inDegree[node] = 0;
- }
+ // Initialize inDegree of all nodes to 0
+ for (let node of this.nodes) {
+ inDegree[node] = 0;
+ }
- // Calculate inDegree of each node
- for (let edge of this.edges) {
- inDegree[edge.to]++;
- }
+ // Calculate inDegree of each node
+ for (let edge of this.edges) {
+ inDegree[edge.to]++;
+ }
- // Collect nodes with 0 inDegree
- for (let node of this.nodes) {
- if (inDegree[node] === 0) {
- zeroInDegreeQueue.push(node);
- }
- }
+ // Collect nodes with 0 inDegree
+ for (let node of this.nodes) {
+ if (inDegree[node] === 0) {
+ zeroInDegreeQueue.push(node);
+ }
+ }
- // process nodes with 0 inDegree
- while (zeroInDegreeQueue.length > 0) {
- let node = zeroInDegreeQueue.shift();
- topologicalOrder.push(node);
+ // process nodes with 0 inDegree
+ while (zeroInDegreeQueue.length > 0) {
+ let node = zeroInDegreeQueue.shift();
+ topologicalOrder.push(node);
- for (let tail of this.getDirectTails(node)) {
- inDegree[tail]--;
- if (inDegree[tail] === 0) {
- zeroInDegreeQueue.push(tail);
- }
- }
- }
- return topologicalOrder;
- }
+ for (let tail of this.getDirectTails(node)) {
+ inDegree[tail]--;
+ if (inDegree[tail] === 0) {
+ zeroInDegreeQueue.push(tail);
+ }
+ }
+ }
+ return topologicalOrder;
+ }
- /**
- * Check if the graph contains a cycle
- * @returns {boolean}
- */
- detectCycle() {
- // If topological order includes all nodes, no cycle exists
- return this.topoSort().length < this.nodes.length;
- }
+ /**
+ * Check if the graph contains a cycle
+ * @returns {boolean}
+ */
+ detectCycle() {
+ // If topological order includes all nodes, no cycle exists
+ return this.topoSort().length < this.nodes.length;
+ }
}
module.exports = myGraph;
diff --git a/utils/jsonTransformer.js b/utils/jsonTransformer.js
index bf070b5..33444c9 100644
--- a/utils/jsonTransformer.js
+++ b/utils/jsonTransformer.js
@@ -1,23 +1,23 @@
/**
* Stringifies only values of a JSON object, including nested ones
- *
+ *
* @param {any} obj JSON object
* @param {string} delimiter Delimiter of final string
* @returns {string}
*/
const getNestedValuesString = (obj, delimiter = ", ") => {
- let values = [];
- for (key in obj) {
- if (typeof obj[key] !== "object") {
- values.push(obj[key]);
- } else {
- values = values.concat(getNestedValuesString(obj[key]));
- }
- }
+ let values = [];
+ for (key in obj) {
+ if (typeof obj[key] !== "object") {
+ values.push(obj[key]);
+ } else {
+ values = values.concat(getNestedValuesString(obj[key]));
+ }
+ }
- return values.join(delimiter);
+ return values.join(delimiter);
}
module.exports = {
- getNestedValuesString
+ getNestedValuesString
}
diff --git a/utils/logger.js b/utils/logger.js
index c2a52b6..96f4412 100644
--- a/utils/logger.js
+++ b/utils/logger.js
@@ -6,31 +6,31 @@ const { combine, label, timestamp, printf, errors } = format;
const typedefs = require("../typedefs");
const getLabel = (callingModule) => {
- if (!callingModule.filename) return "repl";
- const parts = callingModule.filename?.split(path.sep);
- return path.join(parts[parts.length - 2], parts.pop());
+ if (!callingModule.filename) return "repl";
+ const parts = callingModule.filename?.split(path.sep);
+ return path.join(parts[parts.length - 2], parts.pop());
};
const allowedErrorKeys = ["name", "code", "message", "stack"];
const metaFormat = (meta) => {
- if (Object.keys(meta).length > 0)
- return "\n" + JSON.stringify(meta, null, "\t");
- return "";
+ if (Object.keys(meta).length > 0)
+ return "\n" + JSON.stringify(meta, null, "\t");
+ return "";
}
const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
- if (meta.error) { // if the error was passed
- for (const key in meta.error) {
- if (!allowedErrorKeys.includes(key)) {
- delete meta.error[key];
- }
- }
- const { stack, ...rest } = meta.error;
- return `${timestamp} [${label}] ${level}: ${message}${metaFormat(rest)}\n` +
- `${stack ?? ""}`;
+ if (meta.error) { // if the error was passed
+ for (const key in meta.error) {
+ if (!allowedErrorKeys.includes(key)) {
+ delete meta.error[key];
+ }
}
- return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`;
+ const { stack, ...rest } = meta.error;
+ return `${timestamp} [${label}] ${level}: ${message}${metaFormat(rest)}\n` +
+ `${stack ?? ""}`;
+ }
+ return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`;
});
/**
@@ -38,30 +38,30 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
* @param {typedefs.Module} callingModule The module from which the logger is called
*/
const curriedLogger = (callingModule) => {
- let winstonLogger = createLogger({
- levels: config.npm.levels,
- format: combine(
- errors({ stack: true }),
- label({ label: getLabel(callingModule) }),
- timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
- logFormat,
- ),
- transports: [
- new transports.Console({ level: "info" }),
- new transports.File({
- filename: __dirname + "/../logs/debug.log",
- level: "debug",
- maxsize: 10485760,
- }),
- new transports.File({
- filename: __dirname + "/../logs/error.log",
- level: "error",
- maxsize: 1048576,
- }),
- ]
- });
- winstonLogger.on("error", (error) => winstonLogger.error("Error inside logger", { error }));
- return winstonLogger;
+ let winstonLogger = createLogger({
+ levels: config.npm.levels,
+ format: combine(
+ errors({ stack: true }),
+ label({ label: getLabel(callingModule) }),
+ timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
+ logFormat,
+ ),
+ transports: [
+ new transports.Console({ level: "info" }),
+ new transports.File({
+ filename: __dirname + "/../logs/debug.log",
+ level: "debug",
+ maxsize: 10485760,
+ }),
+ new transports.File({
+ filename: __dirname + "/../logs/error.log",
+ level: "error",
+ maxsize: 1048576,
+ }),
+ ]
+ });
+ winstonLogger.on("error", (error) => winstonLogger.error("Error inside logger", { error }));
+ return winstonLogger;
}
module.exports = curriedLogger;
diff --git a/utils/spotifyUriTransformer.js b/utils/spotifyUriTransformer.js
index 9f65491..9611607 100644
--- a/utils/spotifyUriTransformer.js
+++ b/utils/spotifyUriTransformer.js
@@ -11,46 +11,46 @@ const base62Pattern = /^[A-Za-z0-9]+$/;
* @throws {TypeError} If the input is not a valid Spotify URI
*/
const parseSpotifyURI = (uri) => {
- const parts = uri.split(":");
+ const parts = uri.split(":");
- if (parts[0] !== "spotify") {
- throw new TypeError(`${uri} is not a valid Spotify URI`);
- }
+ if (parts[0] !== "spotify") {
+ throw new TypeError(`${uri} is not a valid Spotify URI`);
+ }
- let type = parts[1];
+ let type = parts[1];
- if (type === "local") {
- // Local file format: spotify:local::::
- let idParts = parts.slice(2);
- if (idParts.length < 4) {
- throw new TypeError(`${uri} is not a valid local file URI`);
- }
+ if (type === "local") {
+ // Local file format: spotify:local::::
+ let idParts = parts.slice(2);
+ if (idParts.length < 4) {
+ throw new TypeError(`${uri} is not a valid local file URI`);
+ }
- // URL decode artist, album, and title
- const artist = decodeURIComponent(idParts[0] || "");
- const album = decodeURIComponent(idParts[1] || "");
- const title = decodeURIComponent(idParts[2]);
- const duration = parseInt(idParts[3], 10);
+ // URL decode artist, album, and title
+ const artist = decodeURIComponent(idParts[0] || "");
+ const album = decodeURIComponent(idParts[1] || "");
+ const title = decodeURIComponent(idParts[2]);
+ const duration = parseInt(idParts[3], 10);
- if (isNaN(duration)) {
- throw new TypeError(`${uri} has an invalid duration`);
- }
+ if (isNaN(duration)) {
+ throw new TypeError(`${uri} has an invalid duration`);
+ }
- return { type: "track", is_local: true, artist, album, title, duration };
- } else {
- // Not a local file
- if (parts.length !== 3) {
- throw new TypeError(`${uri} is not a valid Spotify URI`);
- }
+ return { type: "track", is_local: true, artist, album, title, duration };
+ } else {
+ // Not a local file
+ if (parts.length !== 3) {
+ throw new TypeError(`${uri} is not a valid Spotify URI`);
+ }
- const id = parts[2];
+ const id = parts[2];
- if (!base62Pattern.test(id)) {
- throw new TypeError(`${uri} has an invalid ID`);
- }
+ if (!base62Pattern.test(id)) {
+ throw new TypeError(`${uri} has an invalid ID`);
+ }
- return { type, is_local: false, id };
- }
+ return { type, is_local: false, id };
+ }
}
/**
@@ -60,45 +60,45 @@ const parseSpotifyURI = (uri) => {
* @throws {TypeError} If the input is not a valid Spotify link
*/
const parseSpotifyLink = (link) => {
- const localPattern = /^https:\/\/open\.spotify\.com\/local\/([^\/]*)\/([^\/]*)\/([^\/]+)\/(\d+)$/;
- const standardPattern = /^https:\/\/open\.spotify\.com\/([^\/]+)\/([^\/?]+)/;
+ const localPattern = /^https:\/\/open\.spotify\.com\/local\/([^\/]*)\/([^\/]*)\/([^\/]+)\/(\d+)$/;
+ const standardPattern = /^https:\/\/open\.spotify\.com\/([^\/]+)\/([^\/?]+)/;
- if (localPattern.test(link)) {
- // Local file format: https://open.spotify.com/local/artist/album/title/duration
- const matches = link.match(localPattern);
- if (!matches) {
- throw new TypeError(`${link} is not a valid Spotify local file link`);
- }
+ if (localPattern.test(link)) {
+ // Local file format: https://open.spotify.com/local/artist/album/title/duration
+ const matches = link.match(localPattern);
+ if (!matches) {
+ throw new TypeError(`${link} is not a valid Spotify local file link`);
+ }
- // URL decode artist, album, and title
- const artist = decodeURIComponent(matches[1] || "");
- const album = decodeURIComponent(matches[2] || "");
- const title = decodeURIComponent(matches[3]);
- const duration = parseInt(matches[4], 10);
+ // URL decode artist, album, and title
+ const artist = decodeURIComponent(matches[1] || "");
+ const album = decodeURIComponent(matches[2] || "");
+ const title = decodeURIComponent(matches[3]);
+ const duration = parseInt(matches[4], 10);
- if (isNaN(duration)) {
- throw new TypeError(`${link} has an invalid duration`);
- }
+ if (isNaN(duration)) {
+ throw new TypeError(`${link} has an invalid duration`);
+ }
- return { type: "track", is_local: true, artist, album, title, duration };
- } else if (standardPattern.test(link)) {
- // Not a local file
- const matches = link.match(standardPattern);
- if (!matches || matches.length < 3) {
- throw new TypeError(`${link} is not a valid Spotify link`);
- }
+ return { type: "track", is_local: true, artist, album, title, duration };
+ } else if (standardPattern.test(link)) {
+ // Not a local file
+ const matches = link.match(standardPattern);
+ if (!matches || matches.length < 3) {
+ throw new TypeError(`${link} is not a valid Spotify link`);
+ }
- const type = matches[1];
- const id = matches[2];
+ const type = matches[1];
+ const id = matches[2];
- if (!base62Pattern.test(id)) {
- throw new TypeError(`${link} has an invalid ID`);
- }
+ if (!base62Pattern.test(id)) {
+ throw new TypeError(`${link} has an invalid ID`);
+ }
- return { type, is_local: false, id };
- } else {
- throw new TypeError(`${link} is not a valid Spotify link`);
- }
+ return { type, is_local: false, id };
+ } else {
+ throw new TypeError(`${link} is not a valid Spotify link`);
+ }
}
/**
@@ -107,14 +107,14 @@ const parseSpotifyLink = (link) => {
* @returns {string}
*/
const buildSpotifyURI = (uriObj) => {
- if (uriObj.is_local) {
- const artist = encodeURIComponent(uriObj.artist ?? "");
- const album = encodeURIComponent(uriObj.album ?? "");
- const title = encodeURIComponent(uriObj.title ?? "");
- const duration = uriObj.duration ? uriObj.duration.toString() : "";
- return `spotify:local:${artist}:${album}:${title}:${duration}`;
- }
- return `spotify:${uriObj.type}:${uriObj.id}`;
+ if (uriObj.is_local) {
+ const artist = encodeURIComponent(uriObj.artist ?? "");
+ const album = encodeURIComponent(uriObj.album ?? "");
+ const title = encodeURIComponent(uriObj.title ?? "");
+ const duration = uriObj.duration ? uriObj.duration.toString() : "";
+ return `spotify:local:${artist}:${album}:${title}:${duration}`;
+ }
+ return `spotify:${uriObj.type}:${uriObj.id}`;
}
/**
@@ -123,19 +123,19 @@ const buildSpotifyURI = (uriObj) => {
* @returns {string}
*/
const buildSpotifyLink = (uriObj) => {
- if (uriObj.is_local) {
- const artist = encodeURIComponent(uriObj.artist ?? "");
- const album = encodeURIComponent(uriObj.album ?? "");
- const title = encodeURIComponent(uriObj.title ?? "");
- const duration = uriObj.duration ? uriObj.duration.toString() : "";
- return `https://open.spotify.com/local/${artist}/${album}/${title}/${duration}`;
- }
- return `https://open.spotify.com/${uriObj.type}/${uriObj.id}`
+ if (uriObj.is_local) {
+ const artist = encodeURIComponent(uriObj.artist ?? "");
+ const album = encodeURIComponent(uriObj.album ?? "");
+ const title = encodeURIComponent(uriObj.title ?? "");
+ const duration = uriObj.duration ? uriObj.duration.toString() : "";
+ return `https://open.spotify.com/local/${artist}/${album}/${title}/${duration}`;
+ }
+ return `https://open.spotify.com/${uriObj.type}/${uriObj.id}`
}
module.exports = {
- parseSpotifyURI,
- parseSpotifyLink,
- buildSpotifyURI,
- buildSpotifyLink
+ parseSpotifyURI,
+ parseSpotifyLink,
+ buildSpotifyURI,
+ buildSpotifyLink
}
diff --git a/validators/index.js b/validators/index.js
index 91cfca0..c0ff66e 100644
--- a/validators/index.js
+++ b/validators/index.js
@@ -7,40 +7,40 @@ const typedefs = require("../typedefs");
/**
* Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator
- *
- * @param {typedefs.Req} req
- * @param {typedefs.Res} res
- * @param {typedefs.Next} next
+ *
+ * @param {typedefs.Req} req
+ * @param {typedefs.Res} res
+ * @param {typedefs.Next} next
*/
const validate = (req, res, next) => {
- const errors = validationResult(req);
- if (errors.isEmpty()) {
- return next();
- }
+ const errors = validationResult(req);
+ if (errors.isEmpty()) {
+ return next();
+ }
- const extractedErrors = [];
- errors.array().forEach(err => {
- if (err.type === "alternative") {
- err.nestedErrors.forEach(nestedErr => {
- extractedErrors.push({
- [nestedErr.path]: nestedErr.msg
- });
- });
- } else if (err.type === "field") {
- extractedErrors.push({
- [err.path]: err.msg
- });
- }
- });
+ const extractedErrors = [];
+ errors.array().forEach(err => {
+ if (err.type === "alternative") {
+ err.nestedErrors.forEach(nestedErr => {
+ extractedErrors.push({
+ [nestedErr.path]: nestedErr.msg
+ });
+ });
+ } else if (err.type === "field") {
+ extractedErrors.push({
+ [err.path]: err.msg
+ });
+ }
+ });
- res.status(400).json({
- message: getNestedValuesString(extractedErrors),
- errors: extractedErrors
- });
- logger.warn("invalid request", { extractedErrors });
- return;
+ res.status(400).json({
+ message: getNestedValuesString(extractedErrors),
+ errors: extractedErrors
+ });
+ logger.warn("invalid request", { extractedErrors });
+ return;
}
module.exports = {
- validate
+ validate
};
diff --git a/validators/operations.js b/validators/operations.js
index ed503de..82e5111 100644
--- a/validators/operations.js
+++ b/validators/operations.js
@@ -3,29 +3,29 @@ const { body, header, param, query } = require("express-validator");
const typedefs = require("../typedefs");
/**
- * @param {typedefs.Req} req
- * @param {typedefs.Res} res
- * @param {typedefs.Next} next
+ * @param {typedefs.Req} req
+ * @param {typedefs.Res} res
+ * @param {typedefs.Next} next
*/
const createLinkValidator = async (req, res, next) => {
- await body("from")
- .notEmpty()
- .withMessage("from not defined in body")
- .isURL()
- .withMessage("from must be a valid link")
- .run(req);
- await body("to")
- .notEmpty()
- .withMessage("to not defined in body")
- .isURL()
- .withMessage("to must be a valid link")
- .run(req);
- next();
+ await body("from")
+ .notEmpty()
+ .withMessage("from not defined in body")
+ .isURL()
+ .withMessage("from must be a valid link")
+ .run(req);
+ await body("to")
+ .notEmpty()
+ .withMessage("to not defined in body")
+ .isURL()
+ .withMessage("to must be a valid link")
+ .run(req);
+ next();
}
module.exports = {
- createLinkValidator,
- removeLinkValidator: createLinkValidator,
- populateSingleLinkValidator: createLinkValidator,
- pruneSingleLinkValidator: createLinkValidator,
+ createLinkValidator,
+ removeLinkValidator: createLinkValidator,
+ populateSingleLinkValidator: createLinkValidator,
+ pruneSingleLinkValidator: createLinkValidator,
}
diff --git a/validators/playlists.js b/validators/playlists.js
index 13b310e..c9cbad4 100644
--- a/validators/playlists.js
+++ b/validators/playlists.js
@@ -3,20 +3,20 @@ const { body, header, param, query } = require("express-validator");
const typedefs = require("../typedefs");
/**
- * @param {typedefs.Req} req
- * @param {typedefs.Res} res
- * @param {typedefs.Next} next
+ * @param {typedefs.Req} req
+ * @param {typedefs.Res} res
+ * @param {typedefs.Next} next
*/
const getPlaylistDetailsValidator = async (req, res, next) => {
- await query("playlist_link")
- .notEmpty()
- .withMessage("playlist_link not defined in query")
- .isURL()
- .withMessage("playlist_link must be a valid link")
- .run(req);
- next();
+ await query("playlist_link")
+ .notEmpty()
+ .withMessage("playlist_link not defined in query")
+ .isURL()
+ .withMessage("playlist_link must be a valid link")
+ .run(req);
+ next();
}
module.exports = {
- getPlaylistDetailsValidator
+ getPlaylistDetailsValidator
}