mirror of
https://github.com/20kaushik02/spotify-manager.git
synced 2026-01-25 06:04:05 +00:00
editorconfig
This commit is contained in:
@@ -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
|
||||
};
|
||||
sleep, randomBool
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
134
utils/graph.js
134
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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:<artist>:<album>:<title>:<duration>
|
||||
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:<artist>:<album>:<title>:<duration>
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user