karaoke-eternal 1.0.0 → 2.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +10 -10
  2. package/build/client/447.83b0127845c2fa8729fe.js +1 -0
  3. package/build/client/715.83b0127845c2fa8729fe.js +1 -0
  4. package/build/client/718.83b0127845c2fa8729fe.js +1 -0
  5. package/build/client/851.83b0127845c2fa8729fe.js +1 -0
  6. package/build/{845.4be526e3a94d53aeceae.css → client/958.83b0127845c2fa8729fe.css} +53 -6
  7. package/build/client/958.83b0127845c2fa8729fe.js +1 -0
  8. package/build/{index.html → client/index.html} +1 -1
  9. package/build/{licenses.txt → client/licenses.txt} +208 -496
  10. package/build/client/main.83b0127845c2fa8729fe.css +2341 -0
  11. package/build/client/main.83b0127845c2fa8729fe.js +1 -0
  12. package/build/server/Library/Library.js +297 -0
  13. package/build/server/Library/ipc.js +13 -0
  14. package/build/server/Library/router.js +20 -0
  15. package/build/server/Library/socket.js +35 -0
  16. package/build/server/Media/Media.js +170 -0
  17. package/build/server/Media/fileTypes.js +8 -0
  18. package/build/server/Media/ipc.js +13 -0
  19. package/build/server/Media/router.js +97 -0
  20. package/build/server/Player/socket.js +66 -0
  21. package/build/server/Prefs/Prefs.js +181 -0
  22. package/build/server/Prefs/router.js +151 -0
  23. package/build/server/Prefs/socket.js +52 -0
  24. package/build/server/Queue/Queue.js +203 -0
  25. package/build/server/Queue/socket.js +83 -0
  26. package/build/server/Rooms/Rooms.js +171 -0
  27. package/build/server/Rooms/router.js +97 -0
  28. package/build/server/Rooms/socket.js +23 -0
  29. package/build/server/Scanner/FileScanner/FileScanner.js +166 -0
  30. package/build/server/Scanner/FileScanner/getConfig.js +32 -0
  31. package/build/server/Scanner/FileScanner/getFiles.js +61 -0
  32. package/build/server/Scanner/MetaParser/MetaParser.js +77 -0
  33. package/build/server/Scanner/MetaParser/defaultMiddleware.js +170 -0
  34. package/build/server/Scanner/Scanner.js +26 -0
  35. package/build/server/Scanner/ScannerQueue.js +62 -0
  36. package/build/server/User/User.js +206 -0
  37. package/build/server/User/router.js +366 -0
  38. package/build/server/lib/Database.js +39 -0
  39. package/build/server/lib/Errors.js +6 -0
  40. package/build/server/lib/IPCBridge.js +128 -0
  41. package/build/server/lib/Log.js +31 -0
  42. package/build/server/lib/accumulatedThrottle.js +16 -0
  43. package/build/server/lib/bcrypt.js +23 -0
  44. package/build/server/lib/cli.js +131 -0
  45. package/build/server/lib/getCdgName.js +18 -0
  46. package/build/server/lib/getFolders.js +8 -0
  47. package/build/server/lib/getHotMiddleware.js +22 -0
  48. package/build/server/lib/getIPAddress.js +14 -0
  49. package/build/server/lib/getPermutations.js +17 -0
  50. package/build/server/lib/getWindowsDrives.js +17 -0
  51. package/build/server/lib/parseCookie.js +13 -0
  52. package/build/server/lib/pushQueuesAndLibrary.js +22 -0
  53. package/{server → build/server}/lib/schemas/001-initial-schema.sql +26 -26
  54. package/build/server/lib/schemas/004-paths-rooms-data.sql +7 -0
  55. package/build/server/lib/schemas/005-roles.sql +32 -0
  56. package/build/server/lib/util.js +39 -0
  57. package/build/server/main.js +124 -0
  58. package/build/server/scannerWorker.js +59 -0
  59. package/build/server/serverWorker.js +219 -0
  60. package/build/server/socket.js +134 -0
  61. package/build/server/watcherWorker.js +51 -0
  62. package/build/shared/actionTypes.js +113 -0
  63. package/build/shared/types.js +1 -0
  64. package/package.json +111 -86
  65. package/build/267.4be526e3a94d53aeceae.js +0 -1
  66. package/build/591.4be526e3a94d53aeceae.js +0 -1
  67. package/build/598.4be526e3a94d53aeceae.js +0 -1
  68. package/build/799.4be526e3a94d53aeceae.js +0 -1
  69. package/build/845.4be526e3a94d53aeceae.js +0 -1
  70. package/build/main.4be526e3a94d53aeceae.css +0 -2034
  71. package/build/main.4be526e3a94d53aeceae.js +0 -1
  72. package/server/Library/Library.js +0 -340
  73. package/server/Library/index.js +0 -3
  74. package/server/Library/ipc.js +0 -18
  75. package/server/Library/router.js +0 -27
  76. package/server/Library/socket.js +0 -47
  77. package/server/Media/Media.js +0 -207
  78. package/server/Media/index.js +0 -3
  79. package/server/Media/ipc.js +0 -19
  80. package/server/Media/router.js +0 -99
  81. package/server/Player/socket.js +0 -78
  82. package/server/Prefs/Prefs.js +0 -165
  83. package/server/Prefs/index.js +0 -3
  84. package/server/Prefs/router.js +0 -124
  85. package/server/Prefs/socket.js +0 -68
  86. package/server/Queue/Queue.js +0 -208
  87. package/server/Queue/index.js +0 -3
  88. package/server/Queue/socket.js +0 -99
  89. package/server/Rooms/Rooms.js +0 -114
  90. package/server/Rooms/index.js +0 -3
  91. package/server/Rooms/router.js +0 -146
  92. package/server/Scanner/FileScanner/FileScanner.js +0 -225
  93. package/server/Scanner/FileScanner/getConfig.js +0 -35
  94. package/server/Scanner/FileScanner/getFiles.js +0 -63
  95. package/server/Scanner/FileScanner/index.js +0 -3
  96. package/server/Scanner/MetaParser/MetaParser.js +0 -49
  97. package/server/Scanner/MetaParser/defaultMiddleware.js +0 -197
  98. package/server/Scanner/MetaParser/index.js +0 -3
  99. package/server/Scanner/Scanner.js +0 -33
  100. package/server/User/User.js +0 -139
  101. package/server/User/index.js +0 -3
  102. package/server/User/router.js +0 -442
  103. package/server/lib/Database.js +0 -55
  104. package/server/lib/IPCBridge.js +0 -115
  105. package/server/lib/Log.js +0 -71
  106. package/server/lib/bcrypt.js +0 -24
  107. package/server/lib/cli.js +0 -136
  108. package/server/lib/electron.js +0 -81
  109. package/server/lib/getCdgName.js +0 -20
  110. package/server/lib/getDevMiddleware.js +0 -51
  111. package/server/lib/getFolders.js +0 -10
  112. package/server/lib/getHotMiddleware.js +0 -27
  113. package/server/lib/getIPAddress.js +0 -16
  114. package/server/lib/getPermutations.js +0 -21
  115. package/server/lib/getWindowsDrives.js +0 -30
  116. package/server/lib/parseCookie.js +0 -12
  117. package/server/lib/pushQueuesAndLibrary.js +0 -29
  118. package/server/main.js +0 -135
  119. package/server/scannerWorker.js +0 -58
  120. package/server/serverWorker.js +0 -242
  121. package/server/socket.js +0 -173
  122. package/shared/actionTypes.js +0 -103
  123. /package/build/{7ce9eb3fe454f54745a4.woff2 → client/7ce9eb3fe454f54745a4.woff2} +0 -0
  124. /package/build/{598.4be526e3a94d53aeceae.css → client/851.83b0127845c2fa8729fe.css} +0 -0
  125. /package/build/{a35814dd9eb496e3d7cc.woff2 → client/a35814dd9eb496e3d7cc.woff2} +0 -0
  126. /package/build/{e419b95dccb58b362811.woff2 → client/e419b95dccb58b362811.woff2} +0 -0
  127. /package/{server → build/server}/lib/schemas/002-replaygain.sql +0 -0
  128. /package/{server → build/server}/lib/schemas/003-queue-linked-list.sql +0 -0
@@ -0,0 +1,128 @@
1
+ import getLogger from './Log.js';
2
+ import { _ERROR, _SUCCESS } from '../../shared/actionTypes.js';
3
+ const log = getLogger('IPCBridge');
4
+ const PROCESS_NAME = process.env.KES_CHILD_PROCESS || 'main';
5
+ const isParent = typeof process.env.KES_CHILD_PROCESS === 'undefined'; // @todo
6
+ const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
7
+ class IPCParent {
8
+ static children = new Map();
9
+ static handlers = {};
10
+ static send(action, pid) {
11
+ // log.debug(`${PROCESS_NAME} emit: `, action.type)
12
+ if (!this.children.size)
13
+ throw new Error('no child processes');
14
+ if (!pid) {
15
+ this.children.forEach(p => p.send(action));
16
+ return;
17
+ }
18
+ const subprocess = this.children.get(pid);
19
+ if (subprocess)
20
+ subprocess.send(action);
21
+ }
22
+ // 'this' keyword won't work when this method is passed as the
23
+ // message handler callback, so using the class name (IPCParent)
24
+ static handle(action) {
25
+ const { meta, type } = action;
26
+ // log.debug(`${PROCESS_NAME} rcv:`, type)
27
+ if (!type || typeof IPCParent.handlers[type] !== 'function') {
28
+ log.verbose(`${PROCESS_NAME}: no handler for action: ${type}`);
29
+ return;
30
+ }
31
+ // synchronous handler: just fire and forget
32
+ if (!(IPCParent.handlers[type] instanceof AsyncFunction)) {
33
+ IPCParent.handlers[type](action);
34
+ return;
35
+ }
36
+ // async handler: emit the result back to child
37
+ IPCParent.handlers[type](action).then((res) => {
38
+ IPCParent.send({
39
+ ...action,
40
+ type: type + _SUCCESS,
41
+ payload: res,
42
+ }, meta?.pid);
43
+ return;
44
+ }).catch((err) => {
45
+ IPCParent.send({
46
+ ...action,
47
+ type: type + _ERROR,
48
+ error: err,
49
+ }, meta?.pid);
50
+ log.error(`${PROCESS_NAME}: error in ipc action ${type}: ${err.message}`);
51
+ });
52
+ }
53
+ static addChild(subprocess) {
54
+ // parent: handle messages from child process
55
+ subprocess.on('message', action => this.handle(action));
56
+ this.children.set(subprocess.pid, subprocess);
57
+ }
58
+ static removeChild(subprocess) {
59
+ this.children.delete(subprocess.pid);
60
+ }
61
+ static use(obj) {
62
+ this.handlers = {
63
+ ...this.handlers,
64
+ ...obj,
65
+ };
66
+ }
67
+ }
68
+ class IPCChild {
69
+ static handlers = {};
70
+ static requests = {};
71
+ static reqId = 0;
72
+ static send(action) {
73
+ // console.log(`${PROCESS_NAME} emit: `, action.type)
74
+ process.send(action);
75
+ }
76
+ // 'this' keyword won't work when this method is passed as the
77
+ // message handler callback, so using the class name (IPCChild)
78
+ static handle(action) {
79
+ const { error, meta, type } = action;
80
+ // is this a response to a pending request?
81
+ if (meta?.pid === process.pid && IPCChild.requests[meta.reqId]) {
82
+ if (error) {
83
+ IPCChild.requests[meta.reqId].reject(error);
84
+ }
85
+ else {
86
+ IPCChild.requests[meta.reqId].resolve(action.payload);
87
+ }
88
+ // console.log(`${PROCESS_NAME} ack:`, type)
89
+ delete IPCChild.requests[meta.ipcId];
90
+ return;
91
+ }
92
+ // console.log(`${PROCESS_NAME} rcv:`, type)
93
+ // handle request
94
+ if (!type || typeof IPCChild.handlers[type] !== 'function') {
95
+ log.verbose(`${PROCESS_NAME}: no handler for action: ${type}`);
96
+ return;
97
+ }
98
+ IPCChild.handlers[type](action);
99
+ }
100
+ // used by child processes only
101
+ static req(action) {
102
+ const promise = new Promise((resolve, reject) => {
103
+ this.requests[++this.reqId] = { resolve, reject };
104
+ });
105
+ action = {
106
+ ...action,
107
+ meta: {
108
+ ...action.meta,
109
+ reqId: this.reqId,
110
+ pid: process.pid,
111
+ },
112
+ };
113
+ this.send(action);
114
+ return promise;
115
+ }
116
+ static use(obj) {
117
+ this.handlers = {
118
+ ...this.handlers,
119
+ ...obj,
120
+ };
121
+ }
122
+ }
123
+ export default isParent ? IPCParent : IPCChild;
124
+ if (!isParent) {
125
+ // child: handle messages from parent process
126
+ // this also prevents child processes from automatically exiting
127
+ process.on('message', IPCChild.handle);
128
+ }
@@ -0,0 +1,31 @@
1
+ import log from 'electron-log/node.js';
2
+ const LEVELS = [false, 'error', 'warn', 'info', 'verbose', 'debug'];
3
+ class Logger {
4
+ static #instance;
5
+ static init(logId, cfg) {
6
+ // defaults
7
+ log.transports.console.level = 'debug';
8
+ log.transports.file.level = false;
9
+ log.transports.file.fileName = logId + '.log';
10
+ log.transports.file.setAppName('Karaoke Eternal Server');
11
+ for (const transport in cfg) {
12
+ for (const key in cfg[transport]) {
13
+ if (key === 'level')
14
+ log.transports[transport].level = LEVELS[cfg[transport].level];
15
+ else
16
+ log.transports[transport][key] = cfg[transport][key];
17
+ }
18
+ }
19
+ Logger.#instance = log;
20
+ return log;
21
+ }
22
+ static getLogger(scope = '') {
23
+ if (!Logger.#instance)
24
+ throw new Error('logger not initialized');
25
+ return Logger.#instance.scope(scope);
26
+ }
27
+ }
28
+ // for each process/worker to initialize their logger
29
+ export const initLogger = Logger.init;
30
+ // default export
31
+ export default Logger.getLogger;
@@ -0,0 +1,16 @@
1
+ function accumulatedThrottle(callback, wait) {
2
+ let timeoutID;
3
+ let accumulated = [];
4
+ return function (...args) {
5
+ accumulated.push(args);
6
+ if (!timeoutID) {
7
+ timeoutID = setTimeout(function () {
8
+ callback(accumulated);
9
+ accumulated = [];
10
+ clearTimeout(timeoutID);
11
+ timeoutID = undefined;
12
+ }, wait);
13
+ }
14
+ };
15
+ }
16
+ export default accumulatedThrottle;
@@ -0,0 +1,23 @@
1
+ import bcrypt from 'bcrypt';
2
+ function hash(myPlaintextPassword, saltRounds) {
3
+ return new Promise(function (resolve, reject) {
4
+ bcrypt.hash(myPlaintextPassword, saltRounds, function (err, hash) {
5
+ if (err)
6
+ return reject(err);
7
+ return resolve(hash);
8
+ });
9
+ });
10
+ }
11
+ function compare(data, hash) {
12
+ return new Promise(function (resolve, reject) {
13
+ bcrypt.compare(data, hash, function (err, matched) {
14
+ if (err)
15
+ return reject(err);
16
+ return resolve(matched);
17
+ });
18
+ });
19
+ }
20
+ export default {
21
+ hash,
22
+ compare,
23
+ };
@@ -0,0 +1,131 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import fs from 'fs';
5
+ import yargs from 'yargs';
6
+ import { hideBin } from 'yargs/helpers';
7
+ // Resolve package root by walking up to nearest package.json
8
+ function findProjectRoot(startDir) {
9
+ let dir = startDir;
10
+ const root = path.parse(dir).root;
11
+ while (dir !== root) {
12
+ const pkg = path.join(dir, 'package.json');
13
+ if (fs.existsSync(pkg))
14
+ return dir;
15
+ dir = path.dirname(dir);
16
+ }
17
+ return startDir;
18
+ }
19
+ const baseDir = findProjectRoot(path.dirname(fileURLToPath(import.meta.url)));
20
+ const env = {
21
+ NODE_ENV: process.env.NODE_ENV,
22
+ KES_CONSOLE_COLORS: process.env.KES_CONSOLE_COLORS
23
+ ? !['0', 'false'].includes(process.env.KES_CONSOLE_COLORS?.toLowerCase())
24
+ : undefined,
25
+ KES_PATH_ASSETS: path.join(baseDir, 'assets'),
26
+ KES_PATH_DATA: process.env.KES_PATH_DATA || getAppPath('Karaoke Eternal Server'),
27
+ KES_PATH_WEBROOT: path.join(baseDir, 'build', 'client'),
28
+ KES_PORT: parseInt(process.env.KES_PORT, 10) || 0,
29
+ KES_ROTATE_KEY: ['1', 'true'].includes(process.env.KES_ROTATE_KEY?.toLowerCase()),
30
+ KES_SCAN: process.env.KES_SCAN?.trim(),
31
+ KES_SCANNER_CONSOLE_LEVEL: parseInt(process.env.KES_SCANNER_CONSOLE_LEVEL, 10) || undefined,
32
+ KES_SCANNER_LOG_LEVEL: parseInt(process.env.KES_SCANNER_LOG_LEVEL, 10) || undefined,
33
+ KES_SERVER_CONSOLE_LEVEL: parseInt(process.env.KES_SERVER_CONSOLE_LEVEL, 10) || undefined,
34
+ KES_SERVER_LOG_LEVEL: parseInt(process.env.KES_SERVER_LOG_LEVEL, 10) || undefined,
35
+ KES_URL_PATH: process.env.KES_URL_PATH || '/',
36
+ // support PUID/PGID convention
37
+ KES_PUID: parseInt(process.env.PUID, 10) || undefined,
38
+ KES_PGID: parseInt(process.env.PGID, 10) || undefined,
39
+ };
40
+ const argv = yargs(hideBin(process.argv))
41
+ .version(false) // disable default handler
42
+ .option('data', {
43
+ describe: 'Absolute path of folder for database files',
44
+ requiresArg: true,
45
+ type: 'string',
46
+ })
47
+ .option('p', {
48
+ alias: 'port',
49
+ describe: 'Web server port (default=0/auto)',
50
+ number: true,
51
+ requiresArg: true,
52
+ })
53
+ .option('rotateKey', {
54
+ describe: 'Rotate the session key at startup',
55
+ })
56
+ .option('scan', {
57
+ describe: 'Run the media scanner at startup. Accepts a comma-separated list of pathIds, or "all"',
58
+ type: 'string',
59
+ })
60
+ .option('scannerConsoleLevel', {
61
+ describe: 'Media scanner console output level (default=4)',
62
+ number: true,
63
+ requiresArg: true,
64
+ })
65
+ .option('scannerLogLevel', {
66
+ describe: 'Media scanner log file level (default=3)',
67
+ number: true,
68
+ requiresArg: true,
69
+ })
70
+ .option('serverConsoleLevel', {
71
+ describe: 'Web server console output level (default=4)',
72
+ number: true,
73
+ requiresArg: true,
74
+ })
75
+ .option('serverLogLevel', {
76
+ describe: 'Web server log file level (default=3)',
77
+ number: true,
78
+ requiresArg: true,
79
+ })
80
+ .option('urlPath', {
81
+ describe: 'Web server URL base path (default=/)',
82
+ requiresArg: true,
83
+ type: 'string',
84
+ })
85
+ .option('v', {
86
+ alias: 'version',
87
+ describe: 'Output the Karaoke Eternal Server version and exit',
88
+ })
89
+ .usage('$0')
90
+ .usage(' Logging options use the following numeric levels:')
91
+ .usage(' 0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=debug')
92
+ .parseSync();
93
+ if (argv.version) {
94
+ console.log(process.env.npm_package_version);
95
+ process.exit(0); // eslint-disable-line n/no-process-exit
96
+ }
97
+ if (argv.rotateKey) {
98
+ env.KES_ROTATE_KEY = true;
99
+ }
100
+ // CLI options take precedence over env vars
101
+ const opts = {
102
+ data: 'KES_PATH_DATA',
103
+ port: 'KES_PORT',
104
+ scan: 'KES_SCAN',
105
+ scannerConsoleLevel: 'KES_SCANNER_CONSOLE_LEVEL',
106
+ scannerLogLevel: 'KES_SCANNER_LOG_LEVEL',
107
+ serverConsoleLevel: 'KES_SERVER_CONSOLE_LEVEL',
108
+ serverLogLevel: 'KES_SERVER_LOG_LEVEL',
109
+ urlPath: 'KES_URL_PATH',
110
+ };
111
+ for (const opt in opts) {
112
+ if (typeof argv[opt] !== 'undefined') {
113
+ env[opts[opt]] = argv[opt];
114
+ process.env[opts[opt]] = String(argv[opt]);
115
+ }
116
+ }
117
+ export default env;
118
+ function getAppPath(appName) {
119
+ const home = os.homedir ? os.homedir() : process.env.HOME;
120
+ switch (process.platform) {
121
+ case 'darwin': {
122
+ return path.join(home, 'Library', 'Application Support', appName);
123
+ }
124
+ case 'win32': {
125
+ return process.env.APPDATA || path.join(home, 'AppData', 'Roaming', appName);
126
+ }
127
+ default: {
128
+ return process.env.XDG_CONFIG_HOME || path.join(home, '.config', appName);
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,18 @@
1
+ import { promisify } from 'util';
2
+ import fs from 'fs';
3
+ import getPerms from './getPermutations.js';
4
+ const stat = promisify(fs.stat);
5
+ export default async function getCdgName(file) {
6
+ // upper and lowercase permutations since fs may be case-sensitive
7
+ for (const ext of getPerms('cdg')) {
8
+ const cdg = file.substring(0, file.lastIndexOf('.') + 1) + ext;
9
+ try {
10
+ await stat(cdg);
11
+ return cdg;
12
+ }
13
+ catch {
14
+ // try another permutation
15
+ }
16
+ } // end for
17
+ return false;
18
+ }
@@ -0,0 +1,8 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { promisify } from 'util';
4
+ const readdir = promisify(fs.readdir);
5
+ const getFolders = dir => readdir(dir, { withFileTypes: true })
6
+ .then(list => Promise.all(list.map(ent => ent.isDirectory() ? path.resolve(dir, ent.name) : null)))
7
+ .then(list => list.filter(f => !!f).sort());
8
+ export default getFolders;
@@ -0,0 +1,22 @@
1
+ // based on https://github.com/tnnevol/webpack-hot-middleware-for-koa2
2
+ // eslint-disable-next-line n/no-unpublished-import
3
+ import webpackHotMiddleware from 'webpack-hot-middleware';
4
+ export default (compiler, opts) => {
5
+ const middleware = webpackHotMiddleware(compiler, opts);
6
+ return async (ctx, next) => {
7
+ const { end: originalEnd } = ctx.res;
8
+ const runNext = await new Promise((resolve) => {
9
+ ctx.res.end = function () {
10
+ originalEnd.apply(this, arguments);
11
+ resolve(false);
12
+ };
13
+ // call express-style middleware
14
+ middleware(ctx.req, ctx.res, () => {
15
+ resolve(true);
16
+ });
17
+ });
18
+ if (runNext) {
19
+ await next();
20
+ }
21
+ };
22
+ };
@@ -0,0 +1,14 @@
1
+ // https://gist.github.com/savokiss/96de34d4ca2d37cbb8e0799798c4c2d3
2
+ import os from 'os';
3
+ export default function () {
4
+ const interfaces = os.networkInterfaces();
5
+ for (const devName in interfaces) {
6
+ const iface = interfaces[devName];
7
+ for (let i = 0; i < iface.length; i++) {
8
+ const alias = iface[i];
9
+ if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
10
+ return alias.address;
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ // return all uppercase and lowercase permutations of str
2
+ // based on https://stackoverflow.com/a/27995370
3
+ function getPermutations(str) {
4
+ const results = [];
5
+ const arr = str.split('');
6
+ const len = Math.pow(arr.length, 2);
7
+ for (let i = 0; i < len; i++) {
8
+ for (let k = 0, j = i; k < arr.length; k++, j >>= 1) {
9
+ arr[k] = (j & 1) ? arr[k].toUpperCase() : arr[k].toLowerCase();
10
+ }
11
+ const combo = arr.join('');
12
+ results.push(combo);
13
+ }
14
+ // remove duplicates
15
+ return results.filter((ext, pos, self) => self.indexOf(ext) === pos);
16
+ }
17
+ export default getPermutations;
@@ -0,0 +1,17 @@
1
+ import fs from 'fs';
2
+ export default function () {
3
+ const possibleDrives = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(letter => `${letter}:\\`);
4
+ const existingDrives = possibleDrives.filter((drive) => {
5
+ try {
6
+ fs.accessSync(drive, fs.constants.R_OK);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ });
13
+ return existingDrives.map(drive => ({
14
+ path: drive,
15
+ label: drive.substring(0, 2),
16
+ }));
17
+ }
@@ -0,0 +1,13 @@
1
+ // cookie helper based on
2
+ // http://stackoverflow.com/questions/3393854/get-and-set-a-single-cookie-with-node-js-http-server
3
+ export default function parseCookie(cookie) {
4
+ const list = {};
5
+ if (cookie) {
6
+ cookie.split(';')
7
+ .forEach((c) => {
8
+ const parts = c.split('=');
9
+ list[parts.shift().trim()] = decodeURI(parts.join('='));
10
+ });
11
+ }
12
+ return list;
13
+ }
@@ -0,0 +1,22 @@
1
+ import Library from '../Library/Library.js';
2
+ import Queue from '../Queue/Queue.js';
3
+ import Rooms from '../Rooms/Rooms.js';
4
+ import { LIBRARY_PUSH, QUEUE_PUSH } from '../../shared/actionTypes.js';
5
+ async function pushQueuesAndLibrary(io) {
6
+ // emit (potentially) updated queues to each room
7
+ // it's important that this happens before the library is pushed,
8
+ // otherwise queue items might reference newly non-existent songs
9
+ for (const { room, roomId } of Rooms.getActive(io)) {
10
+ io.to(room).emit('action', {
11
+ type: QUEUE_PUSH,
12
+ payload: await Queue.get(roomId),
13
+ });
14
+ }
15
+ // invalidate cache
16
+ Library.cache.version = null;
17
+ io.emit('action', {
18
+ type: LIBRARY_PUSH,
19
+ payload: await Library.get(),
20
+ });
21
+ }
22
+ export default pushQueuesAndLibrary;
@@ -33,23 +33,23 @@ CREATE TABLE IF NOT EXISTS "prefs" (
33
33
 
34
34
  INSERT INTO prefs (key,data) VALUES ('isFirstRun','true');
35
35
 
36
- CREATE TABLE IF NOT EXISTS "queue" (
37
- "queueId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
38
- "roomId" integer NOT NULL REFERENCES rooms(roomId) DEFERRABLE INITIALLY DEFERRED,
39
- "songId" integer NOT NULL,
40
- "userId" integer NOT NULL REFERENCES users(userId) DEFERRABLE INITIALLY DEFERRED
41
- );
42
-
36
+ CREATE TABLE IF NOT EXISTS "queue" (
37
+ "queueId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
38
+ "roomId" integer NOT NULL REFERENCES rooms(roomId) DEFERRABLE INITIALLY DEFERRED,
39
+ "songId" integer NOT NULL,
40
+ "userId" integer NOT NULL REFERENCES users(userId) DEFERRABLE INITIALLY DEFERRED
41
+ );
42
+
43
43
  CREATE INDEX IF NOT EXISTS idxRoom ON "queue" ("roomId" ASC);
44
44
 
45
- CREATE TABLE IF NOT EXISTS "rooms" (
46
- "roomId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
47
- "name" text NOT NULL,
48
- "status" text NOT NULL,
49
- "password" text,
50
- "dateCreated" text NOT NULL
51
- );
52
-
45
+ CREATE TABLE IF NOT EXISTS "rooms" (
46
+ "roomId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
47
+ "name" text NOT NULL,
48
+ "status" text NOT NULL,
49
+ "password" text,
50
+ "dateCreated" integer NOT NULL DEFAULT(0)
51
+ );
52
+
53
53
  CREATE TABLE IF NOT EXISTS "songs" (
54
54
  "songId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
55
55
  "artistId" integer NOT NULL REFERENCES artists(artistId) DEFERRABLE INITIALLY DEFERRED,
@@ -73,17 +73,17 @@ CREATE TABLE IF NOT EXISTS "songStars" (
73
73
 
74
74
  CREATE UNIQUE INDEX IF NOT EXISTS idxUserSong ON "songStars" ("userId" ASC, "songId" ASC);
75
75
 
76
- CREATE TABLE IF NOT EXISTS "users" (
77
- "userId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
78
- "username" text NOT NULL,
79
- "password" text NOT NULL,
80
- "name" text NOT NULL,
81
- "isAdmin" integer(1) NOT NULL,
82
- "image" blob,
83
- "dateCreated" integer NOT NULL DEFAULT(0),
84
- "dateUpdated" integer NOT NULL DEFAULT(0)
85
- );
86
-
76
+ CREATE TABLE IF NOT EXISTS "users" (
77
+ "userId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
78
+ "username" text NOT NULL,
79
+ "password" text NOT NULL,
80
+ "name" text NOT NULL,
81
+ "isAdmin" integer(1) NOT NULL,
82
+ "image" blob,
83
+ "dateCreated" integer NOT NULL DEFAULT(0),
84
+ "dateUpdated" integer NOT NULL DEFAULT(0)
85
+ );
86
+
87
87
  CREATE UNIQUE INDEX IF NOT EXISTS idxUsername ON "users" ("username" ASC);
88
88
 
89
89
  -- Down
@@ -0,0 +1,7 @@
1
+ -- Up
2
+ ALTER TABLE "paths" ADD COLUMN "data" text NOT NULL DEFAULT('{}');
3
+ ALTER TABLE "rooms" ADD COLUMN "data" text NOT NULL DEFAULT('{}');
4
+
5
+ -- Down
6
+ ALTER TABLE "paths" DROP COLUMN "data";
7
+ ALTER TABLE "rooms" DROP COLUMN "data";
@@ -0,0 +1,32 @@
1
+ -- Up
2
+ CREATE TABLE IF NOT EXISTS "roles" (
3
+ "roleId" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
4
+ "name" text NOT NULL COLLATE NOCASE,
5
+ "data" text NOT NULL DEFAULT('{}')
6
+ );
7
+
8
+ CREATE UNIQUE INDEX IF NOT EXISTS idxName ON "roles" ("name" ASC);
9
+
10
+ INSERT INTO roles (name) VALUES ('admin'), ('player'), ('standard'), ('guest');
11
+
12
+ ALTER TABLE users ADD COLUMN "roleId" integer NOT NULL DEFAULT 0 REFERENCES roles(roleId) DEFERRABLE INITIALLY DEFERRED;
13
+
14
+ UPDATE users SET roleId = CASE
15
+ WHEN isAdmin = 1 THEN (SELECT roleId FROM roles WHERE name = 'admin')
16
+ WHEN isAdmin = 0 THEN (SELECT roleId FROM roles WHERE name = 'standard')
17
+ ELSE (SELECT roleId FROM roles WHERE name = 'standard')
18
+ END;
19
+
20
+ ALTER TABLE users DROP COLUMN isAdmin;
21
+
22
+ -- Down
23
+ ALTER TABLE users ADD COLUMN isAdmin integer DEFAULT 0;
24
+
25
+ UPDATE users SET isAdmin = CASE
26
+ WHEN roleId = (SELECT roleId FROM roles WHERE name = 'admin') THEN 1
27
+ ELSE 0
28
+ END;
29
+
30
+ ALTER TABLE users DROP COLUMN roleId;
31
+
32
+ DROP TABLE roles;
@@ -0,0 +1,39 @@
1
+ import path from 'path';
2
+ import crypto from 'crypto';
3
+ /**
4
+ * Gets the normalized file extension, in lowercase and including the period.
5
+ *
6
+ * @param {string} filename The filename to extract the extension from.
7
+ * @returns {string} The extension in lowercase with a period, or an empty string.
8
+ */
9
+ export const getExt = filename => path.extname(filename).toLowerCase();
10
+ export const parsePathIds = (str) => {
11
+ const nums = [];
12
+ // multiple ids?
13
+ if (str && str.includes(',')) {
14
+ const parts = str.split(',');
15
+ for (const part of parts) {
16
+ const n = parseInt(part.trim(), 10);
17
+ if (!isNaN(n))
18
+ nums.push(n);
19
+ }
20
+ }
21
+ else {
22
+ // single id?
23
+ const n = parseInt(str, 10);
24
+ if (!isNaN(n))
25
+ nums.push(n);
26
+ }
27
+ if (nums.length)
28
+ return nums;
29
+ return !!str;
30
+ };
31
+ export const randomChars = (length) => {
32
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
33
+ const bytes = crypto.randomBytes(length);
34
+ let result = '';
35
+ for (let i = 0; i < length; i++) {
36
+ result += chars[bytes[i] % chars.length];
37
+ }
38
+ return result;
39
+ };