[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.

Improve testing, linting, and node usage in devcontainer

* Swtch to devcontainer for node 18.x and remove terminal/launch settings for version (removes nvm dependency in container)
* Update eslint config to use proper config for include/ignore files
* Add spec for mocha and mocha test runner extension + settings

FoxxMD (Oct 8, 2024, 7:33 PM UTC) 6780d722 22d17ba6

+72 -74
+10 -3
.devcontainer/devcontainer.json
··· 3 3 { 4 4 "name": "Node.js", 5 5 // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 - "image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm", 6 + "image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bookworm", 7 7 8 8 // Features to add to the dev container. More info: https://containers.dev/features. 9 9 // "features": {}, 10 10 11 11 // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 - "forwardPorts": [9078] 12 + "forwardPorts": [9078], 13 13 14 14 // Use 'postCreateCommand' to run commands after the container is created. 15 15 //"postCreateCommand": "./.devcontainer/postCreateCommand.sh" 16 16 17 17 // Configure tool-specific properties. 18 - // "customizations": {}, 18 + "customizations": { 19 + "vscode": { 20 + "extensions": [ 21 + "hbenl.vscode-mocha-test-adapter", 22 + "dbaeumer.vscode-eslint" 23 + ] 24 + } 25 + } 19 26 20 27 // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 28 // "remoteUser": "root"
+2 -1
.mocharc.json
··· 1 1 { 2 2 "reporter": "dot", 3 3 "extension": "ts", 4 - "import": "tsx/esm" 4 + "import": "tsx/esm", 5 + "spec": "./src/backend/tests/**/*.test.ts" 5 6 }
-4
.vscode/launch.json
··· 4 4 { 5 5 "name": "dev", 6 6 "type": "node", 7 - "runtimeVersion": "18.19.1", 8 7 "request": "launch", 9 8 // Debug current file in VSCode 10 9 "program": "${workspaceFolder}/src/backend/index.ts", ··· 20 19 { 21 20 "name": "tsx", 22 21 "type": "node", 23 - "runtimeVersion": "18.19.1", 24 22 "request": "launch", 25 23 // Debug current file in VSCode 26 24 "program": "${file}", ··· 47 45 "--recursive", 48 46 "${workspaceFolder}/src/backend/tests/**/*.test.ts" 49 47 ], 50 - "runtimeVersion": "18.19.1", 51 48 "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", 52 49 "internalConsoleOptions": "openOnSessionStart", 53 50 "name": "Mocha Tests", ··· 72 69 "--config", "${workspaceRoot}/.mocharc.json", 73 70 "${file}" 74 71 ], 75 - "runtimeVersion": "18.19.1", 76 72 "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", 77 73 "internalConsoleOptions": "openOnSessionStart", 78 74 "name": "Mocha Test on File",
+3 -3
.vscode/settings.json
··· 1 1 { 2 - "debug.javascript.terminalOptions": { 3 - "runtimeVersion": "18.19.1" 4 - } 2 + "mochaExplorer.require": "tsx/esm", 3 + "mochaExplorer.timeout": 1200000, 4 + "mochaExplorer.exit": true 5 5 }
+29 -36
eslint.config.js
··· 4 4 import tsEslint from 'typescript-eslint'; 5 5 import arrow from 'eslint-plugin-prefer-arrow-functions'; 6 6 7 - export default tsEslint.config( 8 - eslint.configs.recommended, 9 - ...tsEslint.configs.recommended, 10 - // use to enable typed linting (way more errors) https://typescript-eslint.io/linting/typed-linting 11 - /* ...tsEslint.configs.recommendedTypeChecked, 12 - { 13 - languageOptions: { 14 - parserOptions: { 15 - project: true, 16 - tsconfigDirName: import.meta.dirname, 17 - }, 18 - }, 19 - },*/ 20 - { 21 - files: ['src/backend/**/*.ts'], 22 - ignores: ['eslint.config.js'], 23 - plugins: { 24 - "prefer-arrow-functions": arrow 25 - }, 26 - rules: { 27 - 'no-useless-catch': 'off', 28 - '@typescript-eslint/no-unused-vars': 'off', 29 - "prefer-arrow-functions/prefer-arrow-functions": [ 30 - "warn", 31 - { 32 - "allowNamedFunctions": false, 33 - "classPropertiesAllowed": false, 34 - "disallowPrototype": false, 35 - "returnStyle": "unchanged", 36 - "singleReturnOnly": false 37 - } 38 - ], 39 - "arrow-body-style": ["warn", "as-needed"], 40 - "@typescript-eslint/no-explicit-any": "warn" 41 - } 7 + export default tsEslint.config({ 8 + files: ['src/backend/**/*.ts'], 9 + plugins: { 10 + "prefer-arrow-functions": arrow 11 + }, 12 + ignores: [ 13 + 'eslint.config.js', 14 + 'src/backend/tests/**/*.ts' 15 + ], 16 + extends: [ 17 + eslint.configs.recommended, 18 + ...tsEslint.configs.recommended, 19 + ], 20 + rules: { 21 + 'no-useless-catch': 'off', 22 + '@typescript-eslint/no-unused-vars': 'off', 23 + "prefer-arrow-functions/prefer-arrow-functions": [ 24 + "warn", 25 + { 26 + "allowNamedFunctions": false, 27 + "classPropertiesAllowed": false, 28 + "disallowPrototype": false, 29 + "returnStyle": "unchanged", 30 + "singleReturnOnly": false 31 + } 32 + ], 33 + "arrow-body-style": ["warn", "as-needed"], 34 + "@typescript-eslint/no-explicit-any": "warn" 42 35 } 43 - ); 36 + });
+1 -1
package.json
··· 12 12 "schema-aioclient": "typescript-json-schema src/backend/tsconfig.json AIOClientConfig --out src/backend/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage --constAsEnum", 13 13 "circular": "madge --circular --extensions ts src/index.ts", 14 14 "test": "npm run -s test:backend", 15 - "test:backend": "mocha --reporter spec --recursive src/backend/tests/**/*.test.ts", 15 + "test:backend": "mocha --reporter spec", 16 16 "dev": "nodemon -w src/backend -x tsx src/backend/index.ts", 17 17 "start": "NODE_ENV=production tsx src/backend/index.ts", 18 18 "build:frontend": "vite build",
+27 -26
src/backend/tests/component/component.test.ts
··· 23 23 24 24 beforeEach(function() { 25 25 component.config = {}; 26 + // @ts-expect-error should be built on every test 26 27 component.transformRules = undefined; 27 28 }); 28 29 ··· 89 90 90 91 component.buildTransformRules(); 91 92 92 - expect(component.transformRules.preCompare[0]).to.exist; 93 - expect(component.transformRules.preCompare[0].title).to.exist; 94 - expect(Array.isArray(component.transformRules.preCompare[0].title)).is.true; 95 - expect( isConditionalSearchAndReplace(component.transformRules.preCompare[0].title[0])).is.true 93 + expect(component.transformRules.preCompare![0]).to.exist; 94 + expect(component.transformRules.preCompare![0].title).to.exist; 95 + expect(Array.isArray(component.transformRules.preCompare![0].title)).is.true; 96 + expect( isConditionalSearchAndReplace(component.transformRules.preCompare![0].title![0])).is.true 96 97 }); 97 98 98 99 it('Converts transform config into real S&P data with default being empty string', function() { ··· 108 109 109 110 component.buildTransformRules(); 110 111 111 - expect(component.transformRules.preCompare[0]).to.exist; 112 - expect(component.transformRules.preCompare[0].title).to.exist; 113 - expect(Array.isArray(component.transformRules.preCompare[0].title)).is.true; 114 - expect( isConditionalSearchAndReplace(component.transformRules.preCompare[0].title[0])).is.true 115 - expect( component.transformRules.preCompare[0].title[0].search).is.eq('something'); 116 - expect( component.transformRules.preCompare[0].title[0].replace).is.eq(''); 112 + expect(component.transformRules.preCompare![0]).to.exist; 113 + expect(component.transformRules.preCompare![0].title).to.exist; 114 + expect(Array.isArray(component.transformRules.preCompare![0].title)).is.true; 115 + expect( isConditionalSearchAndReplace(component.transformRules.preCompare![0].title![0])).is.true 116 + expect( component.transformRules.preCompare![0].title![0].search).is.eq('something'); 117 + expect( component.transformRules.preCompare![0].title![0].replace).is.eq(''); 117 118 }); 118 119 119 120 it('Respects transform config when it is already S&P data', function() { ··· 134 135 135 136 component.buildTransformRules(); 136 137 137 - expect(component.transformRules.preCompare[0]).to.exist; 138 - expect(component.transformRules.preCompare[0].title).to.exist; 139 - expect(Array.isArray(component.transformRules.preCompare[0].title)).is.true; 140 - expect( isConditionalSearchAndReplace(component.transformRules.preCompare[0].title[0])).is.true 141 - expect( component.transformRules.preCompare[0].title[0].search).is.eq('nothing'); 142 - expect( component.transformRules.preCompare[0].title[0].replace).is.eq('anything'); 138 + expect(component.transformRules.preCompare![0]).to.exist; 139 + expect(component.transformRules.preCompare![0].title).to.exist; 140 + expect(Array.isArray(component.transformRules.preCompare![0].title)).is.true; 141 + expect( isConditionalSearchAndReplace(component.transformRules.preCompare![0].title![0])).is.true 142 + expect( component.transformRules.preCompare![0].title![0].search).is.eq('nothing'); 143 + expect( component.transformRules.preCompare![0].title![0].replace).is.eq('anything'); 143 144 }); 144 145 }); 145 146 ··· 257 258 258 259 const play = generatePlay({artists: ['something', 'big']}); 259 260 const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare); 260 - expect(transformed.data.artists.length).is.eq(1) 261 - expect(transformed.data.artists[0]).is.eq('big') 261 + expect(transformed.data.artists!.length).is.eq(1) 262 + expect(transformed.data.artists![0]).is.eq('big') 262 263 }); 263 264 }); 264 265 ··· 284 285 285 286 const play = generatePlay({artists: ['something', 'big'], album: 'It Has No Match'}); 286 287 const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare); 287 - expect(transformed.data.artists.length).is.eq(2) 288 - expect(transformed.data.artists[0]).is.eq('something') 288 + expect(transformed.data.artists!.length).is.eq(2) 289 + expect(transformed.data.artists![0]).is.eq('something') 289 290 }); 290 291 291 292 it('Does run hook if when conditions matches', function () { ··· 307 308 308 309 const play = generatePlay({artists: ['something', 'big'], album: 'It Has This Match'}); 309 310 const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare); 310 - expect(transformed.data.artists.length).is.eq(1) 311 - expect(transformed.data.artists[0]).is.eq('big') 311 + expect(transformed.data.artists!.length).is.eq(1) 312 + expect(transformed.data.artists![0]).is.eq('big') 312 313 }); 313 314 }); 314 315 ··· 337 338 338 339 const play = generatePlay({artists: ['something', 'big'], album: 'It Has No Match'}); 339 340 const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare); 340 - expect(transformed.data.artists.length).is.eq(2) 341 - expect(transformed.data.artists[0]).is.eq('something') 341 + expect(transformed.data.artists!.length).is.eq(2) 342 + expect(transformed.data.artists![0]).is.eq('something') 342 343 }); 343 344 344 345 it('Does run hook if when conditions matches', function () { ··· 365 366 366 367 const play = generatePlay({artists: ['something', 'big'], album: 'It Has This Match'}); 367 368 const transformed = component.transformPlay(play, TRANSFORM_HOOK.preCompare); 368 - expect(transformed.data.artists.length).is.eq(1) 369 - expect(transformed.data.artists[0]).is.eq('big') 369 + expect(transformed.data.artists!.length).is.eq(1) 370 + expect(transformed.data.artists![0]).is.eq('big') 370 371 }); 371 372 }); 372 373