[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

somewhat working esm conversion

FoxxMD (Jan 18, 2024, 1:15 PM EST) 0591b900 6763be53

+48 -178
+9 -136
package-lock.json
··· 22 22 "@reduxjs/toolkit": "^1.9.5", 23 23 "@supercharge/promise-pool": "^3.0.0", 24 24 "address": "^1.2.2", 25 - "ajv": "^7.2.4", 25 + "ajv": "^8.12.0", 26 26 "avahi-browse": "^1.1.4", 27 27 "better-sse": "^0.8.0", 28 28 "body-parser": "^1.19.0", ··· 122 122 "ts-node": "^10.7.0", 123 123 "tsconfig-paths": "^3.13.0", 124 124 "typedoc": "^0.23.28", 125 - "typescript": "^4.9.5", 125 + "typescript": "^5.3.3", 126 126 "typescript-json-schema": "~0.55", 127 127 "vite": "^5.0.11", 128 128 "vite-tsconfig-paths": "^4.3.1", ··· 6559 6559 } 6560 6560 }, 6561 6561 "node_modules/ajv": { 6562 - "version": "7.2.4", 6563 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.4.tgz", 6564 - "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", 6562 + "version": "8.12.0", 6563 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 6564 + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 6565 6565 "dependencies": { 6566 6566 "fast-deep-equal": "^3.1.1", 6567 6567 "json-schema-traverse": "^1.0.0", ··· 6588 6588 "ajv": { 6589 6589 "optional": true 6590 6590 } 6591 - } 6592 - }, 6593 - "node_modules/ajv-formats/node_modules/ajv": { 6594 - "version": "8.12.0", 6595 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 6596 - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 6597 - "dev": true, 6598 - "dependencies": { 6599 - "fast-deep-equal": "^3.1.1", 6600 - "json-schema-traverse": "^1.0.0", 6601 - "require-from-string": "^2.0.2", 6602 - "uri-js": "^4.2.2" 6603 - }, 6604 - "funding": { 6605 - "type": "github", 6606 - "url": "https://github.com/sponsors/epoberezkin" 6607 6591 } 6608 6592 }, 6609 6593 "node_modules/ansi-colors": { ··· 8950 8934 } 8951 8935 } 8952 8936 }, 8953 - "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { 8954 - "version": "8.12.0", 8955 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 8956 - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 8957 - "dev": true, 8958 - "dependencies": { 8959 - "fast-deep-equal": "^3.1.1", 8960 - "json-schema-traverse": "^1.0.0", 8961 - "require-from-string": "^2.0.2", 8962 - "uri-js": "^4.2.2" 8963 - }, 8964 - "funding": { 8965 - "type": "github", 8966 - "url": "https://github.com/sponsors/epoberezkin" 8967 - } 8968 - }, 8969 8937 "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { 8970 8938 "version": "5.1.0", 8971 8939 "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", ··· 10618 10586 "peerDependencies": { 10619 10587 "eslint": "^7.0.0 || ^8.0.0", 10620 10588 "webpack": "^5.0.0" 10621 - } 10622 - }, 10623 - "node_modules/eslint-webpack-plugin/node_modules/ajv": { 10624 - "version": "8.12.0", 10625 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 10626 - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 10627 - "dev": true, 10628 - "dependencies": { 10629 - "fast-deep-equal": "^3.1.1", 10630 - "json-schema-traverse": "^1.0.0", 10631 - "require-from-string": "^2.0.2", 10632 - "uri-js": "^4.2.2" 10633 - }, 10634 - "funding": { 10635 - "type": "github", 10636 - "url": "https://github.com/sponsors/epoberezkin" 10637 10589 } 10638 10590 }, 10639 10591 "node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": { ··· 17383 17335 "webpack": "^5.0.0" 17384 17336 } 17385 17337 }, 17386 - "node_modules/mini-css-extract-plugin/node_modules/ajv": { 17387 - "version": "8.12.0", 17388 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 17389 - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 17390 - "dev": true, 17391 - "dependencies": { 17392 - "fast-deep-equal": "^3.1.1", 17393 - "json-schema-traverse": "^1.0.0", 17394 - "require-from-string": "^2.0.2", 17395 - "uri-js": "^4.2.2" 17396 - }, 17397 - "funding": { 17398 - "type": "github", 17399 - "url": "https://github.com/sponsors/epoberezkin" 17400 - } 17401 - }, 17402 17338 "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { 17403 17339 "version": "5.1.0", 17404 17340 "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", ··· 24668 24604 } 24669 24605 }, 24670 24606 "node_modules/typescript": { 24671 - "version": "4.9.5", 24672 - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 24673 - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 24607 + "version": "5.3.3", 24608 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 24609 + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 24674 24610 "devOptional": true, 24675 24611 "bin": { 24676 24612 "tsc": "bin/tsc", 24677 24613 "tsserver": "bin/tsserver" 24678 24614 }, 24679 24615 "engines": { 24680 - "node": ">=4.2.0" 24616 + "node": ">=14.17" 24681 24617 } 24682 24618 }, 24683 24619 "node_modules/typescript-json-schema": { ··· 25133 25069 } 25134 25070 } 25135 25071 }, 25136 - "node_modules/vite-tsconfig-paths/node_modules/typescript": { 25137 - "version": "5.3.3", 25138 - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 25139 - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 25140 - "dev": true, 25141 - "optional": true, 25142 - "peer": true, 25143 - "bin": { 25144 - "tsc": "bin/tsc", 25145 - "tsserver": "bin/tsserver" 25146 - }, 25147 - "engines": { 25148 - "node": ">=14.17" 25149 - } 25150 - }, 25151 25072 "node_modules/vite/node_modules/rollup": { 25152 25073 "version": "4.9.5", 25153 25074 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", ··· 25428 25349 "webpack": "^4.0.0 || ^5.0.0" 25429 25350 } 25430 25351 }, 25431 - "node_modules/webpack-dev-middleware/node_modules/ajv": { 25432 - "version": "8.12.0", 25433 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 25434 - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 25435 - "dev": true, 25436 - "dependencies": { 25437 - "fast-deep-equal": "^3.1.1", 25438 - "json-schema-traverse": "^1.0.0", 25439 - "require-from-string": "^2.0.2", 25440 - "uri-js": "^4.2.2" 25441 - }, 25442 - "funding": { 25443 - "type": "github", 25444 - "url": "https://github.com/sponsors/epoberezkin" 25445 - } 25446 - }, 25447 25352 "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { 25448 25353 "version": "5.1.0", 25449 25354 "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", ··· 25541 25446 "dev": true, 25542 25447 "dependencies": { 25543 25448 "@types/node": "*" 25544 - } 25545 - }, 25546 - "node_modules/webpack-dev-server/node_modules/ajv": { 25547 - "version": "8.12.0", 25548 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 25549 - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 25550 - "dev": true, 25551 - "dependencies": { 25552 - "fast-deep-equal": "^3.1.1", 25553 - "json-schema-traverse": "^1.0.0", 25554 - "require-from-string": "^2.0.2", 25555 - "uri-js": "^4.2.2" 25556 - }, 25557 - "funding": { 25558 - "type": "github", 25559 - "url": "https://github.com/sponsors/epoberezkin" 25560 25449 } 25561 25450 }, 25562 25451 "node_modules/webpack-dev-server/node_modules/ajv-keywords": { ··· 26155 26044 }, 26156 26045 "peerDependencies": { 26157 26046 "ajv": ">=8" 26158 - } 26159 - }, 26160 - "node_modules/workbox-build/node_modules/ajv": { 26161 - "version": "8.12.0", 26162 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 26163 - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 26164 - "dev": true, 26165 - "dependencies": { 26166 - "fast-deep-equal": "^3.1.1", 26167 - "json-schema-traverse": "^1.0.0", 26168 - "require-from-string": "^2.0.2", 26169 - "uri-js": "^4.2.2" 26170 - }, 26171 - "funding": { 26172 - "type": "github", 26173 - "url": "https://github.com/sponsors/epoberezkin" 26174 26047 } 26175 26048 }, 26176 26049 "node_modules/workbox-build/node_modules/source-map": {
+3 -2
package.json
··· 1 1 { 2 2 "name": "multi-scrobbler", 3 3 "version": "0.6.3", 4 + "type": "module", 4 5 "description": "scrobble plays from multiple sources to multiple clients", 5 6 "scripts": { 6 7 "schema": "npm run -s schema-aio & npm run -s schema-source & npm run -s schema-client & npm run -s schema-aiosource & npm run -s schema-aioclient", ··· 63 64 "@reduxjs/toolkit": "^1.9.5", 64 65 "@supercharge/promise-pool": "^3.0.0", 65 66 "address": "^1.2.2", 66 - "ajv": "^7.2.4", 67 + "ajv": "^8.12.0", 67 68 "avahi-browse": "^1.1.4", 68 69 "better-sse": "^0.8.0", 69 70 "body-parser": "^1.19.0", ··· 163 164 "ts-node": "^10.7.0", 164 165 "tsconfig-paths": "^3.13.0", 165 166 "typedoc": "^0.23.28", 166 - "typescript": "^4.9.5", 167 + "typescript": "^5.3.3", 167 168 "typescript-json-schema": "~0.55", 168 169 "vite": "^5.0.11", 169 170 "vite-tsconfig-paths": "^4.3.1",
+14 -12
src/backend/common/logging.ts
··· 1 1 import path from "path"; 2 2 import { projectDir } from "./index"; 3 - import winston, {format, LeveledLogMethod, Logger} from '@foxxmd/winston'; 3 + import * as winstonNs from '@foxxmd/winston'; 4 + import winstonDef from '@foxxmd/winston'; 4 5 import {DuplexTransport} from "winston-duplex"; 5 6 import { asLogOptions, LogConfig, LogOptions } from "./infrastructure/Atomic"; 6 7 import process from "process"; ··· 13 14 import {SPLAT, LEVEL, MESSAGE} from 'triple-beam'; 14 15 import { LogInfo, LogLevel } from "../../core/Atomic"; 15 16 import TransportStream from "winston-transport"; 17 + import {format} from 'logform'; 16 18 17 19 const {combine, printf, timestamp, label, splat, errors} = format; 18 20 19 - 20 - const {transports} = winston; 21 + //const {transports} = winstonNew; 22 + const {loggers, transports} = winstonDef; 21 23 22 24 export let logPath = path.resolve(projectDir, `./logs`); 23 25 if (typeof process.env.CONFIG_DIR === 'string') { 24 26 logPath = path.resolve(process.env.CONFIG_DIR, './logs'); 25 27 } 26 28 27 - winston.loggers.add('noop', {transports: [new NullTransport()]}); 29 + loggers.add('noop', {transports: [new NullTransport()]}); 28 30 29 - export const getLogger = (config: LogConfig = {}, name = 'app'): Logger => { 31 + export const getLogger = (config: LogConfig = {}, name = 'app'): winstonNs.Logger => { 30 32 31 - if (!winston.loggers.has(name)) { 33 + if (!loggers.has(name)) { 32 34 const errors: (Error | string)[] = []; 33 35 34 36 let options: LogOptions = {}; ··· 96 98 } 97 99 } 98 100 99 - const loggerOptions: winston.LoggerOptions = { 101 + const loggerOptions: winstonNs.LoggerOptions = { 100 102 level: level, 101 103 format: labelledFormat(), 102 104 transports: myTransports, 103 105 }; 104 106 105 - winston.loggers.add(name, loggerOptions); 107 + loggers.add(name, loggerOptions); 106 108 107 - const logger = winston.loggers.get(name); 109 + const logger = loggers.get(name); 108 110 if (errors.length > 0) { 109 111 for (const e of errors) { 110 112 logger.error(e); ··· 113 115 logger.log 114 116 return logger; 115 117 } 116 - return winston.loggers.get(name); 118 + return loggers.get(name); 117 119 } 118 120 119 121 const breakSymbol = '<br />'; ··· 369 371 } 370 372 371 373 export class MaybeLogger { 372 - logger?: Logger 374 + logger?: winstonNs.Logger 373 375 374 - constructor(logger?: Logger, label?: string) { 376 + constructor(logger?: winstonNs.Logger, label?: string) { 375 377 if (logger !== undefined && label !== undefined) { 376 378 this.logger = logger.child({labels: [label]}, mergeArr); 377 379 } else {
+1 -1
src/backend/common/vendor/KodiApiClient.ts
··· 47 47 48 48 token?: string; 49 49 50 - client: KodiClient; 50 + declare client: KodiClient; 51 51 52 52 constructor(name: any, config: KodiData, options = {}) { 53 53 super('Kodi', name, config, options);
+4 -3
src/backend/server/api.ts
··· 14 14 import {formatLogToHtml, getLogger, isLogLevelMinLevel, isLogLineMinLevel} from "../common/logging"; 15 15 import {MESSAGE} from "triple-beam"; 16 16 import {Transform} from "stream"; 17 - import {createSession} from "better-sse"; 17 + //import {createSession} from "better-sse"; 18 + import * as bsse from 'better-sse'; 18 19 import {setupTautulliRoutes} from "./tautulliRoutes"; 19 20 import {setupPlexRoutes} from "./plexRoutes"; 20 21 import {setupJellyfinRoutes} from "./jellyfinRoutes"; ··· 119 120 } 120 121 121 122 app.get('/api/logs/stream', setLogWebSettings, async (req, res) => { 122 - const session = await createSession(req, res); 123 + const session = await bsse.createSession(req, res); 123 124 await session.stream(logObjectStream); 124 125 }); 125 126 ··· 142 143 }); 143 144 144 145 app.get('/api/events', async (req, res) => { 145 - const session = await createSession(req, res); 146 + const session = await bsse.createSession(req, res); 146 147 scrobbleSources.emitter.on('*', (payload: any, eventName: string) => { 147 148 if(payload.from !== undefined) { 148 149 session.push({event: eventName, ...payload}, payload.from);
+8 -8
src/backend/tsconfig.json
··· 1 1 { 2 + "extends": "../../tsconfig.json", 2 3 "compilerOptions": { 3 - "sourceMap": true, 4 - "resolveJsonModule": true, 4 + "target": "ESNext", 5 5 "emitDecoratorMetadata": true, 6 6 "experimentalDecorators": true, 7 - "lib": ["es2020", "dom"], 8 - "module": "commonjs", 9 - "target": "es2020", 7 + "allowSyntheticDefaultImports": true, 10 8 "esModuleInterop": true, 11 - "skipLibCheck": true, 9 + "lib": [ 10 + "dom", 11 + "dom.iterable", 12 + "esnext" 13 + ], 12 14 13 15 "typeRoots": [ 14 16 "../../node_modules/@types", ··· 17 19 }, 18 20 "include": [ 19 21 "**/*.ts", 20 - "**/*.tsx", 21 - "**/*.js", 22 22 "../core/**/*.ts" 23 23 ], 24 24 "exclude": [
+6 -3
src/backend/utils.ts
··· 4 4 import {Logger} from '@foxxmd/winston'; 5 5 import JSON5 from 'json5'; 6 6 import {TimeoutError, WebapiError} from "spotify-web-api-node/src/response-error.js"; 7 - import Ajv, {Schema} from 'ajv'; 7 + import {Schema} from 'ajv'; 8 + import * as AjvNS from 'ajv'; 9 + import Ajv from 'ajv'; 8 10 import { 9 11 asPlayerStateData, 10 12 NO_DEVICE, ··· 26 28 import {PlayObject} from "../core/Atomic"; 27 29 import address from "address"; 28 30 31 + //const { default: Ajv } = AjvNS; 29 32 dayjs.extend(utc); 30 33 31 34 export async function readJson(this: any, path: any, {throwOnNotFound = true} = {}) { ··· 348 351 } 349 352 350 353 export const createAjvFactory = (logger: Logger): Ajv => { 351 - const validator = new Ajv({logger: logger, verbose: true, strict: "log", allowUnionTypes: true}); 354 + const validator = new AjvNS.default({logger: logger, verbose: true, strict: "log", allowUnionTypes: true}); 352 355 // https://ajv.js.org/strict-mode.html#unknown-keywords 353 356 validator.addKeyword('deprecationMessage'); 354 357 return validator; ··· 364 367 if (Array.isArray(ajv.errors)) { 365 368 for (const err of ajv.errors) { 366 369 let parts = [ 367 - `At: ${err.dataPath}`, 370 + `At: ${err.instancePath}`, 368 371 ]; 369 372 let data; 370 373 if (typeof err.data === 'string') {
+1 -1
src/client/App.tsx
··· 90 90 <a href="/" className="flex items-center flex-grow no-underline pr-4"> 91 91 <img src="icon.svg" style={{maxWidth: '30px'}}/> 92 92 <span className="px-4 break-normal"> 93 - Multi Scrobbler <span className="ml-2 text-xs version">v{process.env.REACT_APP_VERSION ?? 'Unknown'}</span> 93 + Multi Scrobbler <span className="ml-2 text-xs version">v</span> 94 94 </span> 95 95 </a> 96 96 </div>
+1 -1
src/client/tsconfig.json
··· 2 2 "extends": "../../tsconfig.json", 3 3 "compilerOptions": { 4 4 "module": "ESNext", 5 - "moduleResolution": "node", 5 + "moduleResolution": "Bundler", 6 6 "jsx": "react-jsx" 7 7 } 8 8 }
+1 -11
tsconfig.json
··· 2 2 "compilerOptions": { 3 3 "useDefineForClassFields": true, 4 4 "target": "ESNext", 5 - "lib": [ 6 - "dom", 7 - "dom.iterable", 8 - "esnext" 9 - ], 5 + "lib": ["DOM", "DOM.Iterable", "ESNext"], 10 6 "allowJs": false, 11 7 "skipLibCheck": true, 12 8 "esModuleInterop": true, ··· 24 20 }, 25 21 "include": [ 26 22 "src" 27 - ], 28 - "exclude": [ 29 - "src/backend", 30 - "coverage", 31 - "docsite", 32 - "build" 33 23 ], 34 24 }