mirror of
https://github.com/20kaushik02/spotify-manager.git
synced 2026-01-25 06:04:05 +00:00
error handling robustness, logger corrections, better playlist-editable checking
This commit is contained in:
@@ -98,7 +98,8 @@ const singleRequest = async <RespDataType>({
|
|||||||
message = message.concat(
|
message = message.concat(
|
||||||
`${error.response.status} - ${error.response.data?.error?.message}`
|
`${error.response.status} - ${error.response.data?.error?.message}`
|
||||||
);
|
);
|
||||||
res?.status(error.response.status).send(error.response.data);
|
if (res && !res.headersSent)
|
||||||
|
res.status(error.response.status).send(error.response.data);
|
||||||
logger.warn(message, {
|
logger.warn(message, {
|
||||||
response: {
|
response: {
|
||||||
data: error.response.data,
|
data: error.response.data,
|
||||||
@@ -109,13 +110,14 @@ const singleRequest = async <RespDataType>({
|
|||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
// Request sent, but no response received
|
// Request sent, but no response received
|
||||||
message = message.concat("No response");
|
message = message.concat("No response");
|
||||||
res?.status(504).send({ message });
|
if (res && !res.headersSent) res.status(504).send({ message });
|
||||||
logger.error(message, { error });
|
logger.error(message, { error });
|
||||||
return { error, message };
|
return { error, message };
|
||||||
} else {
|
} else {
|
||||||
// Something happened in setting up the request that triggered an Error
|
// Something happened in setting up the request that triggered an Error
|
||||||
message = message.concat("Request failed");
|
message = message.concat("Request failed");
|
||||||
res?.status(500).send({ message: "Internal Server Error" });
|
if (res && !res.headersSent)
|
||||||
|
res.status(500).send({ message: "Internal Server Error" });
|
||||||
logger.error(message, { error });
|
logger.error(message, { error });
|
||||||
return { error, message };
|
return { error, message };
|
||||||
}
|
}
|
||||||
@@ -168,9 +170,7 @@ const getCurrentUsersPlaylistsNextPage: (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
interface GetPlaylistDetailsFirstPageArgs
|
interface GetPlaylistDetailsFirstPageArgs extends EndpointHandlerWithResArgs {
|
||||||
extends Omit<EndpointHandlerWithResArgs, "res"> {
|
|
||||||
res?: Res;
|
|
||||||
initialFields: string;
|
initialFields: string;
|
||||||
playlistID: string;
|
playlistID: string;
|
||||||
}
|
}
|
||||||
@@ -257,9 +257,7 @@ const removePlaylistItems: (
|
|||||||
// non-endpoints, i.e. convenience wrappers
|
// non-endpoints, i.e. convenience wrappers
|
||||||
// ---------
|
// ---------
|
||||||
|
|
||||||
interface CheckPlaylistEditableArgs
|
interface CheckPlaylistEditableArgs extends EndpointHandlerWithResArgs {
|
||||||
extends Omit<EndpointHandlerWithResArgs, "res"> {
|
|
||||||
res?: Res;
|
|
||||||
playlistID: string;
|
playlistID: string;
|
||||||
userID: string;
|
userID: string;
|
||||||
}
|
}
|
||||||
@@ -276,14 +274,13 @@ const checkPlaylistEditable: (
|
|||||||
playlistID,
|
playlistID,
|
||||||
userID,
|
userID,
|
||||||
}) => {
|
}) => {
|
||||||
let checkFields = ["collaborative", "owner(id)"];
|
let checkFields = ["collaborative", "owner(id)", "name"];
|
||||||
let args: GetPlaylistDetailsFirstPageArgs = {
|
const { resp, error, message } = await getPlaylistDetailsFirstPage({
|
||||||
|
res,
|
||||||
authHeaders,
|
authHeaders,
|
||||||
initialFields: checkFields.join(),
|
initialFields: checkFields.join(),
|
||||||
playlistID,
|
playlistID,
|
||||||
};
|
});
|
||||||
if (res) args.res = res;
|
|
||||||
const { resp, error, message } = await getPlaylistDetailsFirstPage(args);
|
|
||||||
if (!resp) return { status: false, error, message };
|
if (!resp) return { status: false, error, message };
|
||||||
|
|
||||||
// https://web.archive.org/web/20241226081630/https://developer.spotify.com/documentation/web-api/concepts/playlists#:~:text=A%20playlist%20can%20also%20be%20made%20collaborative
|
// https://web.archive.org/web/20241226081630/https://developer.spotify.com/documentation/web-api/concepts/playlists#:~:text=A%20playlist%20can%20also%20be%20made%20collaborative
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ const callback: RequestHandler = async (req, res) => {
|
|||||||
Authorization: `Bearer ${req.session.accessToken}`,
|
Authorization: `Bearer ${req.session.accessToken}`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
logger.error("login failed", { statusCode: tokenResponse.status });
|
|
||||||
res
|
res
|
||||||
.status(tokenResponse.status)
|
.status(tokenResponse.status)
|
||||||
.send({ message: "Error: Login failed" });
|
.send({ message: "Error: Login failed" });
|
||||||
|
logger.error("login failed", { statusCode: tokenResponse.status });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,9 +154,7 @@ const updateUser: RequestHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
if (delNum !== deleted.length) {
|
if (delNum !== deleted.length) {
|
||||||
res.status(500).send({ message: "Internal Server Error" });
|
res.status(500).send({ message: "Internal Server Error" });
|
||||||
logger.error("Could not remove all old playlists", {
|
logger.error("Could not remove all old playlists");
|
||||||
error: new Error("Playlists.destroy failed?"),
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,9 +168,7 @@ const updateUser: RequestHandler = async (req, res) => {
|
|||||||
);
|
);
|
||||||
if (addPls.length !== added.length) {
|
if (addPls.length !== added.length) {
|
||||||
res.status(500).send({ message: "Internal Server Error" });
|
res.status(500).send({ message: "Internal Server Error" });
|
||||||
logger.error("Could not add all new playlists", {
|
logger.error("Could not add all new playlists");
|
||||||
error: new Error("Playlists.bulkCreate failed?"),
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,9 +185,7 @@ const updateUser: RequestHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: "Internal Server Error" });
|
res.status(500).send({ message: "Internal Server Error" });
|
||||||
logger.error("Could not update playlist names", {
|
logger.error("Could not update playlist names");
|
||||||
error: new Error("Playlists.update failed?"),
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,13 +266,13 @@ const createLink: RequestHandler = async (req, res) => {
|
|||||||
fromPl = parseSpotifyLink(req.body.from);
|
fromPl = parseSpotifyLink(req.body.from);
|
||||||
toPl = parseSpotifyLink(req.body.to);
|
toPl = parseSpotifyLink(req.body.to);
|
||||||
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
||||||
res.status(400).send({ message: "Link is not a playlist" });
|
res.status(400).send({ message: "Links must be playlist links!" });
|
||||||
logger.info("non-playlist link provided", { from: fromPl, to: toPl });
|
logger.debug("non-playlist link provided");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: "Could not parse link" });
|
res.status(400).send({ message: "Could not parse link" });
|
||||||
logger.warn("parseSpotifyLink", { error });
|
logger.info("parseSpotifyLink", { error });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,8 +285,8 @@ const createLink: RequestHandler = async (req, res) => {
|
|||||||
|
|
||||||
// if playlists are unknown
|
// if playlists are unknown
|
||||||
if (![fromPl, toPl].every((pl) => playlistIDs.includes(pl.id))) {
|
if (![fromPl, toPl].every((pl) => playlistIDs.includes(pl.id))) {
|
||||||
res.status(404).send({ message: "Playlists out of sync." });
|
res.status(404).send({ message: "Unknown playlists, resync first." });
|
||||||
logger.warn("unknown playlists, resync");
|
logger.debug("unknown playlists, resync");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +298,7 @@ const createLink: RequestHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
if (existingLink) {
|
if (existingLink) {
|
||||||
res.status(409).send({ message: "Link already exists!" });
|
res.status(409).send({ message: "Link already exists!" });
|
||||||
logger.info("link already exists");
|
logger.debug("link already exists");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,8 +316,8 @@ const createLink: RequestHandler = async (req, res) => {
|
|||||||
if (newGraph.detectCycle()) {
|
if (newGraph.detectCycle()) {
|
||||||
res
|
res
|
||||||
.status(400)
|
.status(400)
|
||||||
.send({ message: "Proposed link cannot cause a cycle in the graph" });
|
.send({ message: "The link cannot cause a cycle in the graph." });
|
||||||
logger.warn("potential cycle detected");
|
logger.debug("potential cycle detected");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,9 +328,7 @@ const createLink: RequestHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
if (!newLink) {
|
if (!newLink) {
|
||||||
res.status(500).send({ message: "Internal Server Error" });
|
res.status(500).send({ message: "Internal Server Error" });
|
||||||
logger.error("Could not create link", {
|
logger.error("Could not create link");
|
||||||
error: new Error("Links.create failed?"),
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,13 +356,13 @@ const removeLink: RequestHandler = async (req, res) => {
|
|||||||
fromPl = parseSpotifyLink(req.body.from);
|
fromPl = parseSpotifyLink(req.body.from);
|
||||||
toPl = parseSpotifyLink(req.body.to);
|
toPl = parseSpotifyLink(req.body.to);
|
||||||
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
||||||
res.status(400).send({ message: "Link is not a playlist" });
|
res.status(400).send({ message: "Links must be playlist links!" });
|
||||||
logger.info("non-playlist link provided", { from: fromPl, to: toPl });
|
logger.debug("non-playlist link provided");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: "Could not parse link" });
|
res.status(400).send({ message: "Could not parse link" });
|
||||||
logger.warn("parseSpotifyLink", { error });
|
logger.info("parseSpotifyLink", { error });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +374,7 @@ const removeLink: RequestHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
if (!existingLink) {
|
if (!existingLink) {
|
||||||
res.status(409).send({ message: "Link does not exist!" });
|
res.status(409).send({ message: "Link does not exist!" });
|
||||||
logger.warn("link does not exist");
|
logger.debug("link does not exist");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,9 +385,7 @@ const removeLink: RequestHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
if (!removedLink) {
|
if (!removedLink) {
|
||||||
res.status(500).send({ message: "Internal Server Error" });
|
res.status(500).send({ message: "Internal Server Error" });
|
||||||
logger.error("Could not remove link", {
|
logger.error("Could not remove link");
|
||||||
error: new Error("Links.destroy failed?"),
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,12 +550,12 @@ const populateSingleLink: RequestHandler = async (req, res) => {
|
|||||||
toPl = parseSpotifyLink(link.to);
|
toPl = parseSpotifyLink(link.to);
|
||||||
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
||||||
res.status(400).send({ message: "Link is not a playlist" });
|
res.status(400).send({ message: "Link is not a playlist" });
|
||||||
logger.info("non-playlist link provided", link);
|
logger.debug("non-playlist link provided", { link });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: "Could not parse link" });
|
res.status(400).send({ message: "Could not parse link" });
|
||||||
logger.warn("parseSpotifyLink", { error });
|
logger.info("parseSpotifyLink", { error });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,21 +567,21 @@ const populateSingleLink: RequestHandler = async (req, res) => {
|
|||||||
});
|
});
|
||||||
if (!existingLink) {
|
if (!existingLink) {
|
||||||
res.status(409).send({ message: "Link does not exist!" });
|
res.status(409).send({ message: "Link does not exist!" });
|
||||||
logger.warn("link does not exist", { link });
|
logger.debug("link does not exist", { link });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const editableResp = await checkPlaylistEditable({
|
||||||
!(
|
|
||||||
await checkPlaylistEditable({
|
|
||||||
authHeaders,
|
|
||||||
res,
|
res,
|
||||||
|
authHeaders,
|
||||||
playlistID: fromPl.id,
|
playlistID: fromPl.id,
|
||||||
userID: uID,
|
userID: uID,
|
||||||
})
|
});
|
||||||
).status
|
if (!editableResp.status) {
|
||||||
)
|
res.status(403).send({ message: editableResp.message });
|
||||||
|
logger.debug(editableResp.message, { editableResp });
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const fromTracks = await _getPlaylistTracks({
|
const fromTracks = await _getPlaylistTracks({
|
||||||
res,
|
res,
|
||||||
@@ -659,12 +649,12 @@ const populateChain: RequestHandler = async (req, res) => {
|
|||||||
rootPl = parseSpotifyLink(root);
|
rootPl = parseSpotifyLink(root);
|
||||||
if (rootPl.type !== "playlist") {
|
if (rootPl.type !== "playlist") {
|
||||||
res.status(400).send({ message: "Link is not a playlist" });
|
res.status(400).send({ message: "Link is not a playlist" });
|
||||||
logger.info("non-playlist link provided", root);
|
logger.debug("non-playlist link provided");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: "Could not parse link" });
|
res.status(400).send({ message: "Could not parse link" });
|
||||||
logger.warn("parseSpotifyLink", { error });
|
logger.info("parseSpotifyLink", { error });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,14 +681,26 @@ const populateChain: RequestHandler = async (req, res) => {
|
|||||||
const editableStatuses = await Promise.all(
|
const editableStatuses = await Promise.all(
|
||||||
affectedPlaylists.map((pl) => {
|
affectedPlaylists.map((pl) => {
|
||||||
return checkPlaylistEditable({
|
return checkPlaylistEditable({
|
||||||
|
res,
|
||||||
authHeaders,
|
authHeaders,
|
||||||
playlistID: pl,
|
playlistID: pl,
|
||||||
userID: uID,
|
userID: uID,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (editableStatuses.some((statusObj) => statusObj.status === false))
|
if (res.headersSent) return null; // error, resp sent and logged in singleRequest
|
||||||
|
// else, respond with the non-editable playlists
|
||||||
|
const nonEditablePlaylists = editableStatuses.filter(
|
||||||
|
(statusObj) => statusObj.status === false
|
||||||
|
);
|
||||||
|
if (nonEditablePlaylists.length > 0) {
|
||||||
|
let message =
|
||||||
|
"Cannot edit one or more playlists: " +
|
||||||
|
nonEditablePlaylists.map((pl) => pl.error?.playlistName).join(", ");
|
||||||
|
res.status(403).send({ message });
|
||||||
|
logger.debug(message, { nonEditablePlaylists });
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const affectedPlaylistsTracks = await Promise.all(
|
const affectedPlaylistsTracks = await Promise.all(
|
||||||
affectedPlaylists.map((pl) => {
|
affectedPlaylists.map((pl) => {
|
||||||
@@ -830,12 +832,12 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
|
|||||||
toPl = parseSpotifyLink(link.to);
|
toPl = parseSpotifyLink(link.to);
|
||||||
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
if (fromPl.type !== "playlist" || toPl.type !== "playlist") {
|
||||||
res.status(400).send({ message: "Link is not a playlist" });
|
res.status(400).send({ message: "Link is not a playlist" });
|
||||||
logger.info("non-playlist link provided", link);
|
logger.debug("non-playlist link provided");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(400).send({ message: error.message });
|
res.status(400).send({ message: error.message });
|
||||||
logger.warn("parseSpotifyLink", { error });
|
logger.info("parseSpotifyLink", { error });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,17 +853,17 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const editableResp = await checkPlaylistEditable({
|
||||||
!(
|
|
||||||
await checkPlaylistEditable({
|
|
||||||
authHeaders,
|
|
||||||
res,
|
res,
|
||||||
|
authHeaders,
|
||||||
playlistID: toPl.id,
|
playlistID: toPl.id,
|
||||||
userID: uID,
|
userID: uID,
|
||||||
})
|
});
|
||||||
).status
|
if (!editableResp.status) {
|
||||||
)
|
res.status(403).send({ message: editableResp.message });
|
||||||
|
logger.debug(editableResp.message, { editableResp });
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const fromTracks = await _getPlaylistTracks({
|
const fromTracks = await _getPlaylistTracks({
|
||||||
res,
|
res,
|
||||||
|
|||||||
@@ -2,36 +2,22 @@ import path from "path";
|
|||||||
|
|
||||||
import { createLogger, transports, config, format, type Logger } from "winston";
|
import { createLogger, transports, config, format, type Logger } from "winston";
|
||||||
|
|
||||||
const { combine, timestamp, printf, errors } = format;
|
const { combine, timestamp, printf } = format;
|
||||||
|
|
||||||
const metaFormat = (meta: object) => {
|
const metaFormat = (meta: object) => {
|
||||||
const disallowedKeySets = [{ type: Error, keys: ["stack"] }];
|
|
||||||
if (Object.keys(meta).length > 0)
|
if (Object.keys(meta).length > 0)
|
||||||
return (
|
return "\n" + JSON.stringify(meta, null, "\t");
|
||||||
"\n" +
|
|
||||||
JSON.stringify(
|
|
||||||
meta,
|
|
||||||
Object.getOwnPropertyNames(meta).filter((key) => {
|
|
||||||
for (const pair of disallowedKeySets) {
|
|
||||||
if (meta instanceof pair.type) {
|
|
||||||
return !pair.keys.includes(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
"\t"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
|
const logFormat = printf(({ level, message, timestamp, stack, ...meta }) => {
|
||||||
const errorObj: Error = meta["error"] as Error;
|
const errorObj: Error = meta["error"] as Error;
|
||||||
if (errorObj) {
|
if (errorObj) {
|
||||||
|
const stackStr = errorObj["stack"];
|
||||||
return (
|
return (
|
||||||
`${timestamp} [${level.toUpperCase()}]: ${message}` + // line 1
|
`${timestamp} [${level.toUpperCase()}]: ${message}` + // line 1
|
||||||
`${metaFormat(errorObj)}\n` + // metadata
|
`${metaFormat(errorObj)}\n` + // metadata
|
||||||
`${errorObj["stack"] ?? ""}` // stack trace if any
|
`${stackStr}` // stack trace if any
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return `${timestamp} [${level.toUpperCase()}]: ${message}${metaFormat(meta)}`;
|
return `${timestamp} [${level.toUpperCase()}]: ${message}${metaFormat(meta)}`;
|
||||||
@@ -39,11 +25,7 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
|
|||||||
|
|
||||||
const winstonLogger: Logger = createLogger({
|
const winstonLogger: Logger = createLogger({
|
||||||
levels: config.npm.levels,
|
levels: config.npm.levels,
|
||||||
format: combine(
|
format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), logFormat),
|
||||||
errors({ stack: true }),
|
|
||||||
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
||||||
logFormat
|
|
||||||
),
|
|
||||||
transports: [
|
transports: [
|
||||||
new transports.Console({ level: "info" }),
|
new transports.Console({ level: "info" }),
|
||||||
new transports.File({
|
new transports.File({
|
||||||
@@ -58,7 +40,7 @@ const winstonLogger: Logger = createLogger({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
winstonLogger.on("error", (error) =>
|
winstonLogger.on("error", (error) =>
|
||||||
winstonLogger.error("Error inside logger", { error })
|
console.error("Error inside logger", error)
|
||||||
);
|
);
|
||||||
winstonLogger.exceptions.handle(
|
winstonLogger.exceptions.handle(
|
||||||
new transports.File({
|
new transports.File({
|
||||||
|
|||||||
Reference in New Issue
Block a user