Compare commits

...
Author SHA1 Message Date
FoxxMD 46883384b6 docs: Use netlify deploy url if present 2026-04-08 19:46:00 +00:00
FoxxMD 7bc5ab3d0f docs: Specify source for ambiguous components 2026-04-08 19:31:50 +00:00
FoxxMD fd15d15124 dcs: First pass at more descriptive social card text 2026-04-08 19:30:02 +00:00
FoxxMD 14da0795f1 docs: Finalize a working social card strategy
* Helper script to build a static font
* Move all image rendering code into own folder
* Simplify generated text to description OR title
* Use text-wrap balance to make it look prettier
* Add tagline and increase font sizes to fill card dimensions
2026-04-08 19:14:43 +00:00
FoxxMD 3faf0ee7f9 social cards working but need descriptions in each front matter 2026-04-07 15:13:23 +00:00
FoxxMD 78ddb15dcf almost working
svg still doesn't render
2026-04-07 14:14:25 +00:00
FoxxMD 760373c227 docs: Replace instances of foxxmd.github.io 2026-04-07 12:44:41 +00:00
FoxxMD 6bb7b17e97 docs: Update readme with new doc site base url 2026-04-07 12:39:41 +00:00
FoxxMD 231156d452 docs: fix base site env usage 2026-04-07 12:32:31 +00:00
FoxxMD d85628b4c9 docs: Add BASE_SITE env 2026-04-07 12:27:33 +00:00
FoxxMD db0e74aec3 feat(client): Strict ENV types 2026-04-06 20:10:25 +00:00
FoxxMD a918a96d34 feat(sources): Better type enforcement 2026-04-06 20:07:30 +00:00
FoxxMD bb51915085 feat: Increase transform play concurrency
Since MB rate limiting is done by api calient we can safely increase number of plays transformed without risking MB errors from rate limits
2026-04-06 19:12:19 +00:00
Matt Foxx 7c177dfe03 Merge pull request #521 from FoxxMD/processingOverhaul
refactor: Overhaul long-running processes
2026-04-06 15:11:11 -04:00
Matt Foxx cf5e7ca1fe Merge pull request #523 from Druidblack/master
feat: Add Yandex Music source
2026-04-06 14:58:11 -04:00
Druidblack fd43ff86c8 added NO_DEVICE and NO_USER 2026-04-06 21:36:21 +03:00
FoxxMD 194a0e78ff ci: Update checkout and setup-node versions due to node deprecation 2026-04-06 18:28:00 +00:00
FoxxMD 04027e87e7 ci: fix step if statement 2026-04-06 18:04:22 +00:00
FoxxMD a998060bca ci: Add docs repo dispatch 2026-04-06 18:01:10 +00:00
Druidblack 5b16e8857e Removed optional settings 2026-04-06 20:57:36 +03:00
FoxxMD c53ce7c517 docs: Update analytics 2026-04-06 17:03:43 +00:00
FoxxMD 7897dc9503 docs: Add Yandex Music Source docs 2026-04-06 15:04:56 +00:00
Matt Foxx 160ddac3e4 Merge pull request #524 from FoxxMD/docusaursUpdateAndFix
Docusaurs update and fix
2026-04-06 10:35:58 -04:00
FoxxMD 59b4f27294 fix dep changes in wrong file 2026-04-06 14:34:33 +00:00
FoxxMD 21f42bf45b try to trigger netlify 2026-04-06 14:28:08 +00:00
FoxxMD 3e8f170f9a chore(docs): Move markdown link warn into non-deprecated setting 2026-04-06 14:24:27 +00:00
FoxxMD 15fbed09a5 fix(docs): Override lodash to fix assignWith error
https://github.com/lodash/lodash/issues/6167#issuecomment-4172977775
2026-04-06 14:24:10 +00:00
FoxxMD 52b301e729 feat(docs): Update docusaurus to 3.9.2 2026-04-06 14:23:40 +00:00
FoxxMD 0bef197521 feat(yandex): Add env source config 2026-04-06 14:06:05 +00:00
FoxxMD f16750609a feat(yandex): Handle bridge errors as upstream with some response context 2026-04-06 13:58:43 +00:00
FoxxMD c39ef29597 fix: Add missing type to sourcestatusdata 2026-04-06 13:48:49 +00:00
FoxxMD 72639616b7 fix(jellyfin): Fix typo on devicesBlock env assignment #522 2026-04-06 12:53:04 +00:00
Druidblack effe870104 fix time track
fixed the calculation of the track playback if it has ended and the new ones have not started.
2026-04-05 20:56:18 +03:00
Druidblack ee753389f7 Add files via upload 2026-04-05 19:36:13 +03:00
Druidblack 5f4a5cb815 add yandex music 2026-04-05 19:35:00 +03:00
FoxxMD 0607aedf04 feat(musicbrainz): Handle rate limiting within api calls
Instead of using staggerMap with MB transformer settings, handle at api call level for each host
2026-04-02 21:23:26 +00:00
FoxxMD 1f165096b3 feat(client): make scrobble processing abortable 2026-04-02 19:42:59 +00:00
FoxxMD fb6f6a18c8 feat(source): Make discover abortable 2026-04-02 18:41:45 +00:00
FoxxMD 03f8b20704 refactor: Implement abortable source polling
Use abort-controller-x with signals to interrupt polling process

#483
2026-04-02 18:36:20 +00:00
FoxxMD c49cdba718 fix: Initialize plays in correct scope 2026-04-02 01:29:56 +00:00
FoxxMD de67879af5 test: Make loop stop wait time configurable so tests run faster/more stable 2026-04-01 17:10:59 +00:00
FoxxMD 9d0cf8f845 feat: Implement dispose methods for classes with schedulers and long running async tasks 2026-04-01 17:10:25 +00:00
Matt Foxx 8d9fd11c79 Merge pull request #519 from FoxxMD/nodeVersionBump
chore!: Bump node to 24
2026-04-01 11:30:57 -04:00
FoxxMD 745343c589 chore!: Bump node to 24
No breaking changes for MS but local installs will need to update node version
2026-04-01 15:23:40 +00:00
FoxxMD e5935c38da ci: try to force dependabot to use npm 11.12.1 2026-04-01 14:30:44 +00:00
FoxxMD cac13fa3da fix: Align dockefile and devcontainer node/npm versions to fix lockfile mismatch
Mismatch causes npm ci in docker build to error
2026-04-01 13:57:04 +00:00
FoxxMD ca4803713e chore: Update node/npm engine constraints in package.json 2026-04-01 13:16:49 +00:00
FoxxMD e53dd55ad8 chore: Remove unused testing libraries and fix storybook version
* Unusued libraries adding a ton of dependencies for functionality that is not used (yet)
* Need to fix storybook version because 10.3 is unstable
2026-04-01 12:59:08 +00:00
FoxxMD 1f2772353a fix: Fix circular dependency in error utils 2026-04-01 12:59:08 +00:00
Matt Foxx 78b48107a0 Merge pull request #517 from FoxxMD/dependabot/npm_and_yarn/npm_and_yarn-369bcfe21d
chore(deps): bump the npm_and_yarn group across 2 directories with 25 updates
2026-04-01 08:22:07 -04:00
dependabot[bot] bd00055ae5 chore(deps): bump the npm_and_yarn group across 2 directories with 25 updates
Bumps the npm_and_yarn group with 21 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [ajv](https://github.com/ajv-validator/ajv) | `8.17.1` | `8.18.0` |
| [ajv](https://github.com/ajv-validator/ajv) | `6.12.6` | `6.14.0` |
| [formidable](https://github.com/node-formidable/formidable) | `3.5.2` | `3.5.4` |
| [formidable](https://github.com/node-formidable/formidable) | `2.1.2` | `2.1.5` |
| [glob](https://github.com/isaacs/node-glob) | `11.0.0` | `11.1.0` |
| [glob](https://github.com/isaacs/node-glob) | `10.4.5` | `10.5.0` |
| [undici](https://github.com/nodejs/undici) | `6.22.0` | `6.24.1` |
| [undici](https://github.com/nodejs/undici) | `7.10.0` | `7.24.6` |
| [undici](https://github.com/nodejs/undici) | `6.21.3` | `6.24.1` |
| [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) | `10.2.0` | `10.3.3` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `5.4.10` | `5.4.21` |
| [brace-expansion](https://github.com/juliangruber/brace-expansion) | `1.1.11` | `1.1.13` |
| [minimatch](https://github.com/isaacs/minimatch) | `3.1.2` | `3.1.5` |
| [picomatch](https://github.com/micromatch/picomatch) | `2.3.1` | `2.3.2` |
| [axios](https://github.com/axios/axios) | `1.11.0` | `1.14.0` |
| [qs](https://github.com/ljharb/qs) | `6.13.0` | `6.15.0` |
| [yaml](https://github.com/eemeli/yaml) | `1.10.2` | `1.10.3` |
| [flatted](https://github.com/WebReflection/flatted) | `3.3.3` | `3.4.2` |
| [immutable](https://github.com/immutable-js/immutable-js) | `5.1.4` | `5.1.5` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.1.0` | `4.1.1` |
| [jws](https://github.com/brianloveswords/node-jws) | `4.0.0` | `4.0.1` |
| [lodash](https://github.com/lodash/lodash) | `4.17.21` | `4.18.0` |
| [on-headers](https://github.com/jshttp/on-headers) | `1.0.2` | `1.1.0` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `6.27.0` | `6.30.3` |
| [rollup](https://github.com/rollup/rollup) | `4.24.3` | `4.60.1` |
| [tmp](https://github.com/raszi/node-tmp) | `0.0.33` | `0.2.5` |

Bumps the npm_and_yarn group with 13 updates in the /docsite directory:

| Package | From | To |
| --- | --- | --- |
| [undici](https://github.com/nodejs/undici) | `6.21.1` | `6.24.1` |
| [brace-expansion](https://github.com/juliangruber/brace-expansion) | `1.1.11` | `1.1.13` |
| [minimatch](https://github.com/isaacs/minimatch) | `3.1.2` | `3.1.5` |
| [picomatch](https://github.com/micromatch/picomatch) | `2.3.1` | `2.3.2` |
| [yaml](https://github.com/eemeli/yaml) | `2.8.2` | `2.8.3` |
| [immutable](https://github.com/immutable-js/immutable-js) | `5.1.4` | `5.1.5` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `3.14.1` | `3.14.2` |
| [lodash](https://github.com/lodash/lodash) | `4.17.21` | `4.18.0` |
| [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) | `13.2.0` | `13.2.1` |
| [on-headers](https://github.com/jshttp/on-headers) | `1.0.2` | `1.1.0` |
| [node-forge](https://github.com/digitalbazaar/forge) | `1.3.1` | `1.4.0` |
| [svgo](https://github.com/svg/svgo) | `3.3.2` | `3.3.3` |
| [webpack](https://github.com/webpack/webpack) | `5.97.1` | `5.105.4` |



Updates `ajv` from 8.17.1 to 8.18.0
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v8.17.1...v8.18.0)

Updates `ajv` from 6.12.6 to 6.14.0
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v8.17.1...v8.18.0)

Updates `formidable` from 3.5.2 to 3.5.4
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits)

Updates `formidable` from 2.1.2 to 2.1.5
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits)

Updates `glob` from 11.0.0 to 11.1.0
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.0...v11.1.0)

Updates `glob` from 10.4.5 to 10.5.0
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.0...v11.1.0)

Updates `undici` from 6.22.0 to 6.24.1
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.24.1)

Updates `undici` from 7.10.0 to 7.24.6
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.24.1)

Updates `undici` from 6.21.3 to 6.24.1
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.24.1)

Updates `storybook` from 10.2.0 to 10.3.3
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.3/code/core)

Updates `vite` from 5.4.10 to 5.4.21
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

Updates `brace-expansion` from 1.1.11 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.13)

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `axios` from 1.11.0 to 1.14.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.14.0)

Updates `qs` from 6.13.0 to 6.15.0
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.15.0)

Updates `yaml` from 1.10.2 to 1.10.3
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v1.10.2...v1.10.3)

Updates `flatted` from 3.3.3 to 3.4.2
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

Updates `immutable` from 5.1.4 to 5.1.5
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.4...v5.1.5)

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

Updates `jws` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v4.0.0...v4.0.1)

Updates `lodash` from 4.17.21 to 4.18.0
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.0)

Updates `on-headers` from 1.0.2 to 1.1.0
- [Release notes](https://github.com/jshttp/on-headers/releases)
- [Changelog](https://github.com/jshttp/on-headers/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0)

Updates `react-router` from 6.27.0 to 6.30.3
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router@6.30.3/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@6.30.3/packages/react-router)

Updates `rollup` from 4.24.3 to 4.60.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.24.3...v4.60.1)

Updates `tmp` from 0.0.33 to 0.2.5
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.0.33...v0.2.5)

Updates `undici` from 6.21.1 to 6.24.1
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.24.1)

Updates `brace-expansion` from 1.1.11 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.13)

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `yaml` from 2.8.2 to 2.8.3
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v1.10.2...v1.10.3)

Updates `immutable` from 5.1.4 to 5.1.5
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.4...v5.1.5)

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

Updates `lodash` from 4.17.21 to 4.18.0
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.0)

Updates `mdast-util-to-hast` from 13.2.0 to 13.2.1
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

Updates `on-headers` from 1.0.2 to 1.1.0
- [Release notes](https://github.com/jshttp/on-headers/releases)
- [Changelog](https://github.com/jshttp/on-headers/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0)

Updates `node-forge` from 1.3.1 to 1.4.0
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.4.0)

Updates `svgo` from 3.3.2 to 3.3.3
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v3.3.2...v3.3.3)

Updates `webpack` from 5.97.1 to 5.105.4
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.97.1...v5.105.4)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 8.18.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: formidable
  dependency-version: 3.5.4
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: formidable
  dependency-version: 2.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: glob
  dependency-version: 11.1.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 7.24.6
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: storybook
  dependency-version: 10.3.3
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: axios
  dependency-version: 1.14.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: qs
  dependency-version: 6.15.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: yaml
  dependency-version: 1.10.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: jws
  dependency-version: 4.0.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: lodash
  dependency-version: 4.18.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: react-router
  dependency-version: 6.30.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-version: 4.60.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tmp
  dependency-version: 0.2.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: yaml
  dependency-version: 2.8.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: lodash
  dependency-version: 4.18.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: node-forge
  dependency-version: 1.4.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: svgo
  dependency-version: 3.3.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: webpack
  dependency-version: 5.105.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 20:31:04 +00:00
FoxxMD b174b23b3b fix: re-install missing dependencies introduced by dependabot 2026-03-31 20:07:34 +00:00
Matt Foxx 9e6319ee73 Configure Dependabot for npm and docker updates
Updated package ecosystems for Dependabot to include npm and docker.
2026-03-31 15:59:51 -04:00
Matt Foxx 6b5b3990cd Merge pull request #511 from FoxxMD/dependabot/npm_and_yarn/npm_and_yarn-6582722f31
chore(deps): bump the npm_and_yarn group across 2 directories with 2 updates
2026-03-31 15:53:42 -04:00
dependabot[bot] f2465f0f88 chore(deps): bump the npm_and_yarn group across 2 directories with 2 updates
Bumps the npm_and_yarn group with 1 update in the / directory: [express](https://github.com/expressjs/express).
Bumps the npm_and_yarn group with 2 updates in the /docsite directory: [express](https://github.com/expressjs/express) and [path-to-regexp](https://github.com/pillarjs/path-to-regexp).


Updates `express` from 4.21.1 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...v4.22.1)

Updates `path-to-regexp` from 0.1.10 to 0.1.13
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13)

Updates `express` from 4.21.1 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...v4.22.1)

Updates `path-to-regexp` from 0.1.10 to 0.1.13
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13)

Updates `express` from 4.21.2 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...v4.22.1)

Updates `path-to-regexp` from 0.1.12 to 0.1.13
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13)

Updates `express` from 4.21.2 to 4.22.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...v4.22.1)

Updates `path-to-regexp` from 0.1.12 to 0.1.13
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-version: 0.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-version: 0.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-version: 0.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-version: 0.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 19:50:21 +00:00
FoxxMD c68ec8dbf6 chore: Remove unused nyc dependency 2026-03-31 19:46:03 +00:00
FoxxMD 6e1cdba58c chore(release): Bump version for release 2026-03-31 14:45:29 +00:00
FoxxMD df4bb27d3a docs: Add music assistant to readme 2026-03-31 14:44:43 +00:00
FoxxMD de7707b45a docs: Simplify installation
* Remove docker run
* Condense docker settings into tabs
2026-03-31 14:43:42 +00:00
FoxxMD c884b4776c docs: Add grafana dashboard 2026-03-31 14:06:19 +00:00
FoxxMD 0893b62f3b docs: Fix discord link on jellyfin page 2026-03-31 14:06:19 +00:00
FoxxMD 7d071c763d docs: Add more caching guidance 2026-03-31 14:06:19 +00:00
Matt Foxx 664395a0ee Merge pull request #508 from FoxxMD/cacheAusterity
feat: Reduce cache memory usage
2026-03-31 09:01:46 -04:00
FoxxMD 007d553d59 fix missed ttl for mb tests 2026-03-30 19:24:57 +00:00
FoxxMD 7c8c185c4c refactor: Remove ttl from mb api to ensure primary/secondary ttl is used correctly 2026-03-30 18:59:37 +00:00
FoxxMD 972eccd0bf cache init cleanup 2026-03-30 18:50:54 +00:00
Matt Foxx 034cfe7a8e Merge pull request #509 from FoxxMD/netlifyPreview
Netlify preview
2026-03-30 11:25:10 -04:00
FoxxMD 7a8cf7c004 feat: Skip fetching backlogged plays entirely if scrobbleBacklog=false 2026-03-29 23:50:55 +00:00
FoxxMD 30cdc9f553 docs: Add prometheus usage 2026-03-29 23:50:55 +00:00
Matt Foxx 3f19abb899 Merge pull request #506 from FoxxMD/musicAssistant
Music assistant
2026-03-29 19:30:50 -04:00
FoxxMD a601757759 docs: Add netlify badge to footer on preview docs 2026-03-28 22:16:02 +00:00
FoxxMD fe03bcd651 refactor: Refactor cache building to be able to specify both primary and secondary 2026-03-28 21:36:18 +00:00
FoxxMD 1f319cb9f6 fix: Use lifecycle invariant play for closest match 2026-03-28 02:01:14 +00:00
FoxxMD a0ac0f8e46 reduce step ttl to 2 minutes 2026-03-27 20:30:48 +00:00
FoxxMD 06945fda52 feat: Improve cache stats
* Remove vsize since its very inaccurate
* Use memory store `size` for count, if present
2026-03-27 20:30:32 +00:00
FoxxMD 306c4c858e fix: Only enable metrics collection for real runs 2026-03-27 18:51:00 +00:00
FoxxMD 81885632a0 feat: Add cache stats to prometheus metrics 2026-03-27 18:45:30 +00:00
FoxxMD 23467481c1 docs: Add prometheus usage 2026-03-27 17:47:15 +00:00
Matt Foxx e716de2160 Add Contributor Covenant Code of Conduct
This document outlines the Contributor Covenant Code of Conduct, including our pledge, standards, enforcement responsibilities, and guidelines for community behavior.
2026-03-27 13:15:33 -04:00
FoxxMD dda06a0873 add chunk array function 2026-03-27 16:08:47 +00:00
FoxxMD eca64d68e7 fix(musicbrainz): Catch recordings with no releases 2026-03-27 16:06:43 +00:00
Matt Foxx 1065643b23 Merge pull request #493 from FoxxMD/deadScrobbleImprovements
feat: Dead scrobble improvements
2026-03-26 08:58:31 -04:00
FoxxMD d92153f85f fix: Fix sleep time between consecutive dead scrobble attempts 2026-03-26 08:40:16 -04:00
FoxxMD 22d76c459a fix(scrobble): Improve ui/api experience for dead scrobble processing
* Return 200 when processing all, don't wait to finish to prevent response timeout
* Use event stream to update/remove/add dead scrobbles in ui

#489
2026-03-26 08:40:16 -04:00
FoxxMD ec87ab96d6 fix(scrobble): Improve dead scrobble iteration and logging
* Use filtered, processable scrobbles in for-loop to avoid any weird in-place array splice issues when iterating
* Add more logging with labels/ids to make logging more readable
* Move dead scrobble removal function closer blocks where it should happen
2026-03-26 08:40:13 -04:00
FoxxMD f62fe270fb feat(scrobble): Increase default dead letter retries from 1 to 3
#488
2026-03-26 08:36:54 -04:00
Matt Foxx 677474beed Merge pull request #504 from FoxxMD/transformStepCachingFix
fix: transform step usage and caching fix
2026-03-25 23:27:23 -04:00
FoxxMD cb9dd0fbc5 docs: Add Music Assistant to lz endpoint docs 2026-03-25 20:20:50 +00:00
FoxxMD 0685d4b06d feat(lzendpoint): Update json body parsing to support Music Assistant 2026-03-25 20:19:29 +00:00
FoxxMD 82c0a4faec fix: Refactor play tranforming to dogfood steps and fix caching #495
* Re-use lifecycle steps/patch to build diffs
* Break out step generation into own function to simplify logic
* Cache steps instead of play to avoid returning a cached play when data is the same #495
* Replace patch object/patching logic with json-diff-ts as its more robust and actually maintained
  * Reduces need to stringify everywhere due to lack of cloning in jsonpatchdiff
2026-03-25 18:48:47 +00:00
FoxxMD ccbd5e304a fix: Better handling when stage fails but should continue 2026-03-25 10:13:06 -04:00
FoxxMD 4f81a192fa Merge branch 'storybook' 2026-03-24 21:39:22 -04:00
FoxxMD 22cf95e58c feat(listenbrainz): Use upstream for backlogged 2026-03-25 01:37:54 +00:00
FoxxMD 358355c612 refactor: Move stagger config responsibility to transformer 2026-03-25 00:41:10 +00:00
FoxxMD c5f9600047 docs: Add duplicate scrobble detection explanation 2026-03-24 23:54:55 +00:00
Matt Foxx 703731e654 Merge pull request #501 from FoxxMD/storybook
More UI
2026-03-24 16:39:38 -04:00
FoxxMD 0dfa4c7aad fix(test): Fix expected plays for YTM interim plays 2026-03-24 20:23:47 +00:00
FoxxMD 68e473ec02 fix: Add stagger options as injected dependency to make tests faster 2026-03-24 20:21:14 +00:00
FoxxMD a8d20734aa fix: Fix wrong operation for maxRandomStagger 2026-03-24 20:20:37 +00:00
FoxxMD ebdb76cf6c fix: Fix type-erased meta property for json plays 2026-03-24 20:19:27 +00:00
FoxxMD e62e1ac2dd feat: Add seenAt to source/client play meta 2026-03-24 19:38:05 +00:00
FoxxMD e68b46f647 feat(ui): Implement play activity sortby seen/played 2026-03-24 19:15:54 +00:00
FoxxMD 7605cd3f1d virtualization is messy and plain accordian works better 2026-03-24 18:56:07 +00:00
FoxxMD 2735c283ce virtualized activity log 2026-03-24 17:47:51 +00:00
FoxxMD 0daaec81ca feat(ui): Add more info to play data 2026-03-24 01:22:39 +00:00
FoxxMD 2085df4406 feat(ui): Dynamic expand/collapse all 2026-03-24 00:16:57 +00:00
FoxxMD 9b8664d3a3 feat(ui): Condense timeline items into title using collapsible 2026-03-24 00:02:42 +00:00
FoxxMD e027a33e8d feat(ui): Play Data rows for better use of wide viewports 2026-03-23 18:56:50 +00:00
FoxxMD f04a19d34d feat(ui): Add flow and step state to timeline 2026-03-23 18:32:28 +00:00
FoxxMD 4b27671a91 feat: Add step and flow result to lifecycle 2026-03-23 18:32:12 +00:00
FoxxMD 5f4943e025 fix(ui): Fix keys 2026-03-20 20:47:38 +00:00
FoxxMD 66cdb94d57 feat(ui): Add lifecycle step story and fix diff rendering 2026-03-20 20:39:35 +00:00
FoxxMD 5ebb0e6a9c refactor: Separating functions so they are usuable by frontend storybook 2026-03-20 19:33:09 +00:00
FoxxMD dfb7f43c00 feat: Implement play with lifecycle fixture 2026-03-20 18:24:04 +00:00
FoxxMD ed85d35117 feat: Implement random object generator 2026-03-20 16:31:19 +00:00
FoxxMD 7d69e77ac6 Implement proper play conversion for frontend/backend testing
Convert to json-equivalent object, and back, without needing json stringify/parse
2026-03-20 15:22:53 +00:00
FoxxMD 03973d215b refactor: Fix JsonPlayObject to properly type dates 2026-03-20 14:00:17 +00:00
Matt Foxx 1b182d89cd Merge pull request #498 from FoxxMD/storybook
feat: Add storybook and initial ui design POC
2026-03-20 09:40:48 -04:00
FoxxMD 3d5c2c7450 fix: Remove schema-plugin types hint from docusaurus config 2026-03-20 13:31:51 +00:00
FoxxMD d6264dd994 chore: ignore storybook folders for docker image 2026-03-20 13:31:33 +00:00
FoxxMD 4c18123207 refactor: Move some utils into core folder 2026-03-20 13:29:23 +00:00
FoxxMD 71e7f41ea5 more ui improvements 2026-03-20 01:02:25 +00:00
FoxxMD 60cf5d1019 feat(ui): Hide text-heavy elements for small mobile 2026-03-19 21:11:11 +00:00
FoxxMD 3977057dfa more tweaks 2026-03-19 20:36:48 +00:00
FoxxMD 943bdc3e88 feat(ui): Improve play data 2026-03-19 20:23:15 +00:00
FoxxMD 44710048f4 feat(ui): Integrate timeline into activity detail 2026-03-19 19:48:22 +00:00
FoxxMD 9f9f5655b1 feat(ui): Implement scrobble action 2026-03-19 18:38:39 +00:00
FoxxMD e621811364 feat(ui): Implement duplicate match check 2026-03-19 18:10:26 +00:00
FoxxMD 685bc846e8 feat(ui): Implement transform steps 2026-03-19 17:36:02 +00:00
FoxxMD 3a9d58fae9 feat(ui): Implement error stack and more things 2026-03-19 02:07:09 +00:00
FoxxMD 33639c6e4c more improvements 2026-03-18 20:59:53 +00:00
FoxxMD 7c235e3179 feat(ui): Add PlayInfo to list 2026-03-18 20:29:39 +00:00
FoxxMD 559d749207 feat(ui): More activity log improvements 2026-03-18 20:12:01 +00:00
FoxxMD 53feae0f8c more imrovements 2026-03-18 19:04:51 +00:00
FoxxMD 0941ccc0ac improved list layout 2026-03-18 18:33:16 +00:00
FoxxMD 82ecd96ba4 Containerized playinfo 2026-03-18 18:19:38 +00:00
FoxxMD e118dc5db1 Better args default for stories 2026-03-18 18:19:27 +00:00
FoxxMD 1a23491ac2 feat(ui): Set max height for code block with scrolling 2026-03-18 17:56:38 +00:00
FoxxMD d916f167a5 feat(ui): Implement code block toggle for play info 2026-03-18 17:35:43 +00:00
FoxxMD 0102249549 feat(ui): Implement PlayInfo component 2026-03-18 16:44:17 +00:00
FoxxMD 2ca96dab42 Init storybook with chakra-ui 2026-03-18 10:25:20 -04:00
FoxxMD 345e60b7a3 chore(release): Bump version for release 2026-03-16 13:33:06 +00:00
Matt Foxx b9fb6c9d35 Merge pull request #487 from FoxxMD/jsonView
feat(docs): Implement rich config exploration and interaction
2026-03-13 15:00:29 -04:00
FoxxMD 5d0ccd8867 docs: re-nable AIO config display as read-only on mobile 2026-03-13 14:52:29 -04:00
FoxxMD b36f484f3b chore: Add netlify-specific build command 2026-03-13 14:52:29 -04:00
FoxxMD d2f7f986c7 Add gitkeep for schema folder 2026-03-13 14:52:29 -04:00
FoxxMD 9137d03b9f use genericonfiguration component for remaining client/sources 2026-03-13 14:52:29 -04:00
FoxxMD f6c0d13a8b feat(docs): Implement config example/structure guidance 2026-03-13 14:52:17 -04:00
FoxxMD c472b4e33a refactor: reorganized and consolidate source/client types 2026-03-13 14:52:17 -04:00
FoxxMD f6c3fc6b50 refactor(docs): replace docusaurus json-schema-plugin
* Use cf-json-schema-viz for schema viewer
* Use modern-react-json-editor for editing
2026-03-13 14:50:39 -04:00
FoxxMD 8ba146495e feat(jellyin): Add external URL guidance
#486
2026-03-12 14:00:59 +00:00
Matt Foxx 68c46fefb2 Merge pull request #471 from FoxxMD/componentPagination
feat: Implement pagination fetching for component listen data
2026-03-12 08:46:36 -04:00
Matt Foxx eddda41559 Merge pull request #473 from owendaprile/push-ovmumqzwvmqx
fix(plex): Also read MBIDs from `guid` field
2026-03-11 14:08:36 -04:00
FoxxMD 5945fdc2ef docs(spotify): Add premium account guidance
#485
2026-03-11 17:59:27 +00:00
FoxxMD d110dcbdcf fix(sources): Add a confidence signal to plays generated from authoratiative sources to avoid fuzzy matching repeats
https://github.com/FoxxMD/multi-scrobbler/issues/430#issuecomment-4035670777
2026-03-11 13:54:24 +00:00
Owen D'Aprile ebc0aa8a4e fix(plex): Also read MBIDs from guid field
Some items on Plex store the MBID in the `guid` field in the `Metadata`
response, not in the `Guid` field. This change checks the `guid` field
as a fallback if the `Guid` field is not set.
2026-03-10 20:24:25 -04:00
FoxxMD d4113b464a feat(scrobbler): Improved dupe detection
* Check queue before adding so that previously cached or re-discovered plays from a source are not added multiple times to the queue (should only be unique plays)
* Add timestamp heuristic to dupe matching -- if score is *close* to matching and timestamp is *exact* then nudge it into a match
2026-03-10 20:37:14 +00:00
FoxxMD 753dfe1fb3 feat: Implement better request retry logic
* Use p-retry to standard retry behavior
* Simplify/remove other own-rolled retry logic
* Fix superagent request instantiation
  * Requests are mutable, cannot be retried once executed. Need to make a new request for each retry
2026-03-10 19:47:42 +00:00
FoxxMD 013cccaac5 add more logging for discord now playing update logic 2026-03-10 16:43:49 +00:00
FoxxMD 504f8ffbeb refactor(source): Reduce logging noise for activity checks
Move most activity checks to TRACE log level to reduce noise at DEBUG level
2026-03-10 16:06:55 +00:00
FoxxMD 81d1eb2553 feat(ui): Default logs to trace 2026-03-10 15:33:54 +00:00
FoxxMD c9327b0e92 fix(lastfm): Default to not including now playing in history response
Scrobbler getScrobblesForTimeRange function isn't aware of the context and we only want now playing for the Source impl anyway.

Fixes not scrobbling play because it is also now playing which mathced existing scrobble logic.

https://github.com/FoxxMD/multi-scrobbler/issues/430#issuecomment-4031615361
2026-03-10 15:00:13 +00:00
FoxxMD 36aa236d82 use TRACE for cache file loading chunk logging 2026-03-09 19:44:58 +00:00
FoxxMD 3c37aa4449 fix(lastfm): Fix not returning nowPlaying scrobble 2026-03-09 18:39:33 +00:00
FoxxMD f1bf3cbe64 fix(nowPlaying): Scrobbler should report now playing if player state is known nowPlayingMode source
#477#issuecomment-4022857648
2026-03-09 18:39:11 +00:00
FoxxMD 17a9356040 fix: Handle historical scrobble fetch failure
* Don't cache error
* Handle error with logging and put scrobble into dead queue (or increment retries on dead)
2026-03-09 16:47:11 +00:00
FoxxMD 97c799bf2c refactor: Re-implement retry logic for LZ with fixed request init 2026-03-09 16:46:10 +00:00
FoxxMD 99c02daec7 fix(koito): Assign cursor to page param 2026-03-09 16:45:30 +00:00
FoxxMD 4cb792641d fix(pagination): Assign cursor on initial request and properly catch errors to add request parameter logging 2026-03-09 16:45:13 +00:00
FoxxMD fbc2950854 fix upstream scrobble tests 2026-03-06 20:47:04 +00:00
FoxxMD 08e0bdb437 feat(scrobbler): re-implement scrobble preload
Use a limit/maxFetch upper limit to return a bulk of scrobbles by count, rather than timerange, so that backlogged scrobbles are less likely to cause many fetches on first startup
2026-03-06 20:28:22 +00:00
FoxxMD 73dcab4de1 fix(scrobbler): Fix ttl for timerange cahe and missing awaits 2026-03-06 19:42:15 +00:00
FoxxMD 00594a14ed feat: Implement timerange consolidation 2026-03-06 19:41:54 +00:00
FoxxMD 52d5bab0f5 Implement noop logger 2026-03-06 19:40:50 +00:00
FoxxMD a41f4c56f7 feat(scrobbler): Implement play-dependent, arbitrary timerange fetching for duplicate checks
* Remove design bias towards "recent" scrobbles with limited timerange and complex shouldRefresh logic
* Replace with logic to fetch scrobbles from timeranges that are relevant to queued/dead scrobbles
  * timeranges are grouped by closely timestamped queued scrobbles
  * timeranges are cached based on staleAfter
2026-03-06 16:57:09 +00:00
Matt Foxx 8ea83962d6 Merge pull request #482 from FoxxMD/namedTransformFix
fix(transform): Fixes for user transform and transform selection
2026-03-06 08:15:34 -05:00
FoxxMD 9d97cc6e4c fix(transform): Fix transform for stage not using name for selection #481 2026-03-05 15:49:51 +00:00
FoxxMD 023579de9a refactor(atproto): Replace naive TID impl with @ewanc26/tid 2026-03-04 21:00:35 +00:00
FoxxMD 214d99f1c3 fix(lastfm): Check for singular scrobble response 2026-03-04 21:00:35 +00:00
FoxxMD d6fc6b9b10 debug for lastfm list type 2026-03-04 21:00:35 +00:00
FoxxMD cbdab93f92 refactor(tealfm): Replace TID package to avoid node-gyp dependency 2026-03-04 21:00:35 +00:00
FoxxMD abf37f0687 refactor(spotify): use listen timerange fetcher 2026-03-04 21:00:35 +00:00
FoxxMD e993a43399 refactor: Implement generic cursor iteration 2026-03-04 21:00:35 +00:00
FoxxMD bc79969fd0 refactor(maloja): use listen timerange fetcher 2026-03-04 21:00:35 +00:00
FoxxMD 801f9ed403 refactor(koito): Use listen timerange fetcher 2026-03-04 21:00:34 +00:00
FoxxMD 3d9f6f8f58 refactor(lastfm): Use listen timerange fetcher 2026-03-04 21:00:34 +00:00
FoxxMD c2676fb620 refactor(listenbrainz): Use listen timerange fetcher 2026-03-04 21:00:34 +00:00
FoxxMD cdb4ab23e9 refactor(listenbrainz)!: Remove Koito support
Koito functionality has diverged enough that it doesn't make sense to provide partial support via LZ components.

This also makes timerange pagination easier.
2026-03-04 21:00:33 +00:00
FoxxMD b3217f2d9c mostly implemented time range fetch utility 2026-03-04 20:59:52 +00:00
FoxxMD 7b61385ad5 implement time range scrobbles for teal 2026-03-04 20:59:52 +00:00
FoxxMD b5c86e74ad more pagination implementation 2026-03-04 20:59:51 +00:00
FoxxMD a3031a7eb9 Somewhat working
Stole pagination from FoxxMD/multi-scrobbler#390
2026-03-04 20:59:32 +00:00
FoxxMD 7c49a0535f fix typings 2026-03-04 20:36:35 +00:00
FoxxMD 797e5b0794 feat(scrobbler): Add dupe match debug information to lifecycle 2026-03-04 20:30:03 +00:00
FoxxMD bc6cd78c86 refactor: Implement alreadyScrobbled in base class
None of the child classes use a different implementation. DRY
2026-03-04 18:40:27 +00:00
FoxxMD 98a5b40be5 refactor: Use trace logging for defaults
* All logger use trace as minimum level
* Force app log stream to trace minimum so we always show all possible logs in dashboard
* Add trace level to dashboard
* Make dashboard start in debug logs for readability
2026-03-04 18:22:41 +00:00
FoxxMD 87c174fe0f chore: exclude schema folder from search 2026-03-04 15:52:49 +00:00
Matt Foxx 0cbb45e4ad Merge pull request #478 from FoxxMD/discordHangingStatus
fix(discord): Add hidden activity type
2026-03-04 10:41:11 -05:00
FoxxMD b11226af94 fix(discord): Don't throw on unknown activity type
This isn't used for filtering anymore so its not necessary to validate to a known activity
2026-03-04 15:40:14 +00:00
Matt Foxx 27dc5b5e19 Merge pull request #479 from FoxxMD/userTransformDefaults
fix(transform): Fix missing defaults assignment for stage config parsing
2026-03-04 08:12:58 -05:00
FoxxMD d892b83110 fix(transform): Fix user transform default value 2026-03-03 22:00:42 +00:00
FoxxMD c28984524a fix(transform): Fix missing defaults assignment for stage config parsing 2026-03-03 21:04:40 +00:00
FoxxMD 5d4fb63baa fix(discord): Add hidden activity type
Fixes #477
2026-03-03 13:50:14 +00:00
FoxxMD ca2574d1e5 Add comment pointing to JF source on MB ids 2026-03-02 15:45:43 +00:00
FoxxMD 758038e278 fix: Missed dependency bump 2026-02-27 16:57:24 +00:00
FoxxMD cb803b1a24 feat: Use lower minimum log level to prepare for logging cleanup 2026-02-27 16:51:30 +00:00
FoxxMD c985af5722 docs: Add discord to readme 2026-02-27 14:19:41 +00:00
FoxxMD 1ede54acbb docs: Fix broken links and anchors 2026-02-27 14:18:50 +00:00
FoxxMD a73ca6a95d feat(ui): Add service status page link to dashboard 2026-02-27 02:45:12 +00:00
301 changed files with 27225 additions and 8936 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ services:
# context: .
# dockerfile: .devcontainer/Dockerfile
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
image: mcr.microsoft.com/devcontainers/javascript-node:24-bookworm
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
+1 -1
View File
@@ -20,7 +20,7 @@
"forwardPorts": [9078,3000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "npm install",
"postCreateCommand": ".devcontainer/postCreate.sh",
// Configure tool-specific properties.
"customizations": {
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
echo 'Adding node_modules to path'
echo 'export PATH=/workspaces/node_modules/.bin:/workspaces/docsite/node_modules/.bin:$PATH' >> /home/node/.bashrc
echo 'Updating npm...'
npm install -g npm@11.12.1
echo 'Installing concurrently...'
npm install -g concurrently
echo 'If you have freshly cloned this project or node_modules folder does not exist then you should run "npm run install:parallel" now'
+3 -1
View File
@@ -28,4 +28,6 @@ build
**/act/*
tmp-*
.env
*.env
*.env
.storybook
.stories
+19
View File
@@ -0,0 +1,19 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directories:
- "/"
- "/docsite"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `root` directory
directory: "/"
# Check for updates once a week
schedule:
interval: "weekly"
+20 -7
View File
@@ -12,12 +12,12 @@ jobs:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
with:
node-version: 20
node-version: 24
cache: npm
- name: Install app dev dependencies
@@ -28,13 +28,11 @@ jobs:
- name: Install docusaurus dependencies
run: npm ci
working-directory: ./docsite
- name: Build website
env:
ANALYTICS: ${{ vars.ANALYTICS }}
ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }}
RY_ANALYTICS: ${{ vars.RY_ANALYTICS }}
RY_ANALYTICS_SITEID: ${{ vars.RY_ANALYTICS_SITEID }}
RY_ANALYTICS_REPLAY: ${{ vars.RY_ANALYTICS_REPLAY }}
GSITEVERIFICATION: ${{ vars.GSITEVERIFICATION }}
DOCS_BASE: '/multi-scrobbler'
run: npm run build
@@ -43,7 +41,7 @@ jobs:
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 #3.9.3
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e #4.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Build output to publish to the `gh-pages` branch:
@@ -55,3 +53,18 @@ jobs:
# You can swap them out with your own user credentials.
user_name: github-actions[bot]
user_email: 41898282+github-actions[bot]@users.noreply.github.com
- name: Trigger dependent build
if: ${{ vars.DOCS_REPO != ''}}
run: |
# Trigger builds in dependent repositories
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
"https://api.github.com/repos/${{ vars.DOCS_REPO }}/dispatches" \
-d "{
\"event_type\": \"release\",
\"client_payload\": {
\"commit\": \"${{ github.sha }}\"
}
}"
+1 -1
View File
@@ -72,7 +72,7 @@ jobs:
- name: Check out the repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
+2 -2
View File
@@ -19,12 +19,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
+5
View File
@@ -134,6 +134,7 @@ src/backend/common/schema/*.json
!src/backend/common/schema/aio.json
!src/backend/common/schema/client.json
!src/backend/common/schema/source.json
docsite/static/schemas/*.json
*.bak
@@ -147,3 +148,7 @@ build
**/generated-sources.*
.github/act/out
tmp-*
*storybook.log
storybook-static
lib
+1 -1
View File
@@ -1 +1 @@
v18.19.1
v24.14.0
-24
View File
@@ -1,24 +0,0 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"exclude": [
"node_modules/",
"**/src/backend/common/schema/**",
"**/src/backend/tests/**",
"register.js",
"**/src/server/**/*.d.ts",
"**/src/client/**"
],
"include": [
"**/src/backend/**/*.ts",
"**/src/**/backend/*.js",
"**/src/**/backend/*.js.map"
],
"extension": [
".ts"
],
"reporter": [
"text-summary",
"html"
],
"report-dir": "./coverage"
}
+45
View File
@@ -0,0 +1,45 @@
import { defineMain } from "@storybook/react-vite/node";
import { mergeConfig } from "vite";
export default defineMain({
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": [
"@chromatic-com/storybook",
// "@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs",
// "@storybook/addon-onboarding"
],
features: {
// https://github.com/storybookjs/storybook/discussions/33279
sidebarOnboardingChecklist: false,
},
"framework": "@storybook/react-vite",
viteFinal: async (config) => {
return mergeConfig(config, {
optimizeDeps: {
include: ["@storybook/addon-docs"],
},
plugins: [
{
name: "fix-mdx-react-shim",
enforce: "pre",
resolveId(source: any) {
if (
source.startsWith("file://") &&
source.includes("mdx-react-shim.js")
) {
// Convert file:///... path to normal filesystem path for Vite
return new URL(source).pathname;
}
return null;
},
},
],
});
},
});
+6
View File
@@ -0,0 +1,6 @@
import { addons } from 'storybook/manager-api';
import { themes } from 'storybook/theming';
addons.setConfig({
theme: themes.dark,
});
+28
View File
@@ -0,0 +1,28 @@
import addonDocs from "@storybook/addon-docs";
import addonA11y from "@storybook/addon-a11y";
import { definePreview } from "@storybook/react-vite";
import { themes } from 'storybook/theming';
export default definePreview({
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo'
},
docs: {
theme: themes.dark,
},
},
addons: [addonA11y(), addonDocs()]
});
+2 -1
View File
@@ -4,7 +4,8 @@
"mochaExplorer.exit": true,
"search.exclude": {
"**/.docusaurus": true,
"**/build": true
"**/build": true,
"**/schema": true
},
"tailwindCSS.files.exclude": [
"docsite/**"
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
coc@foxxmd.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+3 -1
View File
@@ -1,7 +1,7 @@
FROM ghcr.io/linuxserver/baseimage-debian:bookworm AS base
ENV TZ=Etc/GMT
ENV NODE_VERSION=20.18.1
ENV NODE_VERSION=24.14.0
# borrowing openssl header removal trick from offical docker-node
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
@@ -49,6 +49,8 @@ RUN \
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "Node: $(node -v)\nNPM: $(npm -v)"
RUN npm install -g concurrently
ARG data_dir=/config
+54 -52
View File
@@ -3,7 +3,7 @@
[![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)](https://github.com/FoxxMD/multi-scrobbler/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/multi-scrobbler)](https://hub.docker.com/r/foxxmd/multi-scrobbler)
[![Docs](https://img.shields.io/badge/Read%20The%20Docs-1082c2)](https://foxxmd.github.io/multi-scrobbler/)
[![Docs](https://img.shields.io/badge/Read%20The%20Docs-1082c2)](https://docs.multi-scrobbler.app/)
<img src="/assets/icon.png" align="right"
@@ -11,54 +11,56 @@ alt="multi-scrobbler logo" width="180" height="180">
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
* Supports scrobbling from many [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources)
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/azuracast)
* [Deezer](https://foxxmd.github.io/multi-scrobbler/configuration/sources/deezer)
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/google-cast)
* [Icecast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/icecast)
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jellyfin)
* [JRiver](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jriver)
* [Kodi](https://foxxmd.github.io/multi-scrobbler/configuration/sources/kodi)
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/sources/koito-source)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source)
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-endpoint)
* [Libre.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/librefm-source)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source)
* [ListenBrainz (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-endpoint)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/sources/maloja)
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mopidy)
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpd)
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpris)
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/configuration/sources/musikcube)
* [Plex](https://foxxmd.github.io/multi-scrobbler/configuration/sources/plex)
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/sources/rocksky-source)
* [Sonos](https://foxxmd.github.io/multi-scrobbler/configuration/sources/sonos)
* [Spotify](https://foxxmd.github.io/multi-scrobbler/configuration/sources/spotify)
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/tealfm-source)
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/configuration/sources/webscrobbler)
* [VLC](https://foxxmd.github.io/multi-scrobbler/configuration/sources/vlc)
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/yamaha-musiccast)
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/configuration/sources/youtube-music)
* Supports scrobbling to many [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients)
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/clients/koito)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/lastfm)
* [Libre.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/librefm)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/clients/listenbrainz)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/clients/maloja)
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/clients/rocksky)
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/tealfm)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/configuration#webhook-configurations), [healthcheck endpoints](https://foxxmd.github.io/multi-scrobbler/configuration#health-endpoint), or [prometheus metrics](https://foxxmd.github.io/multi-scrobbler/configuration/#prometheus).
* Supports [Now Playing](https://foxxmd.github.io/multi-scrobbler/configuration/clients#now-playing) for scrobble Clients
* Supports scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
* [Azuracast](https://docs.multi-scrobbler.app/configuration/sources/azuracast)
* [Deezer](https://docs.multi-scrobbler.app/configuration/sources/deezer)
* [Google Cast (Chromecast)](https://docs.multi-scrobbler.app/configuration/sources/google-cast)
* [Icecast](https://docs.multi-scrobbler.app/configuration/sources/icecast)
* [Jellyfin](https://docs.multi-scrobbler.app/configuration/sources/jellyfin)
* [JRiver](https://docs.multi-scrobbler.app/configuration/sources/jriver)
* [Kodi](https://docs.multi-scrobbler.app/configuration/sources/kodi)
* [Koito](https://docs.multi-scrobbler.app/configuration/sources/koito-source)
* [Last.fm](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source)
* [Last.fm (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/lastfm-endpoint)
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/sources/librefm-source)
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source)
* [ListenBrainz (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint)
* [Maloja](https://docs.multi-scrobbler.app/configuration/sources/maloja)
* [Mopidy](https://docs.multi-scrobbler.app/configuration/sources/mopidy)
* [MPD (Music Player Daemon)](https://docs.multi-scrobbler.app/configuration/sources/mpd)
* [MPRIS (Linux Desktop)](https://docs.multi-scrobbler.app/configuration/sources/mpris)
* [Musikcube](https://docs.multi-scrobbler.app/configuration/sources/musikcube)
* [Music Assistant](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint#music-assistant)
* [Plex](https://docs.multi-scrobbler.app/configuration/sources/plex)
* [Rocksky](https://docs.multi-scrobbler.app/configuration/sources/rocksky-source)
* [Sonos](https://docs.multi-scrobbler.app/configuration/sources/sonos)
* [Spotify](https://docs.multi-scrobbler.app/configuration/sources/spotify)
* [Subsonic-compatible APIs](https://docs.multi-scrobbler.app/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [teal.fm](https://docs.multi-scrobbler.app/configuration/sources/tealfm-source)
* [WebScrobbler](https://docs.multi-scrobbler.app/configuration/sources/webscrobbler)
* [VLC](https://docs.multi-scrobbler.app/configuration/sources/vlc)
* [Yamaha MusicCast](https://docs.multi-scrobbler.app/configuration/sources/yamaha-musiccast)
* [Youtube Music](https://docs.multi-scrobbler.app/configuration/sources/youtube-music)
* Supports scrobbling to many [**Clients**](https://docs.multi-scrobbler.app/configuration/clients)
* [Discord](https://docs.multi-scrobbler.app/configuration/clients/discord) (Now Playing)
* [Koito](https://docs.multi-scrobbler.app/configuration/clients/koito)
* [Last.fm](https://docs.multi-scrobbler.app/configuration/clients/lastfm)
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/clients/librefm)
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/clients/listenbrainz)
* [Maloja](https://docs.multi-scrobbler.app/configuration/clients/maloja)
* [Rocksky](https://docs.multi-scrobbler.app/configuration/clients/rocksky)
* [teal.fm](https://docs.multi-scrobbler.app/configuration/clients/tealfm)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://docs.multi-scrobbler.app/configuration#webhook-configurations), [healthcheck endpoints](https://docs.multi-scrobbler.app/configuration#health-endpoint), or [prometheus metrics](https://docs.multi-scrobbler.app/configuration/#prometheus).
* Supports [Now Playing](https://docs.multi-scrobbler.app/configuration/clients#now-playing) for scrobble Clients
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/configuration/transforms)
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/installation#nodejs)
* Modify data before scrobbling with [regular expression or search patterns](https://docs.multi-scrobbler.app/configuration/transforms)
* Install using [Docker images for x86/ARM](https://docs.multi-scrobbler.app/installation#docker) or [locally with NodeJS](https://docs.multi-scrobbler.app/installation#nodejs)
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler//quickstart)
[**Quick Start Guide**](https://docs.multi-scrobbler.app//quickstart)
<img src="/docsite/static/img/status-ui.png" width="800">
@@ -71,15 +73,15 @@ A dockerized app that monitors your music listening activity from *everywhere* a
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
Yes! You can use [Last.fm as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
## How Does multi-scrobbler (MS) Work?
You set up [configurations](https://foxxmd.github.io/multi-scrobbler/configuration) for one or more [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) and one or more [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
You set up [configurations](https://docs.multi-scrobbler.app/configuration) for one or more [**Sources**](https://docs.multi-scrobbler.app/configuration/sources) and one or more [**Clients**](https://docs.multi-scrobbler.app/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
### Source
A [**Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
A [**Source**](https://docs.multi-scrobbler.app/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
Source configurations consist of:
@@ -89,7 +91,7 @@ Source configurations consist of:
### Client
A [**Client**](https://foxxmd.github.io/multi-scrobbler/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
A [**Client**](https://docs.multi-scrobbler.app/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
Client configurations consist of:
@@ -98,15 +100,15 @@ Client configurations consist of:
## Quick Start
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/quickstart)
[See the **Quick Start Guide**](https://docs.multi-scrobbler.app/quickstart)
## Installation
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/installation)
[See the **Installation** documentation](https://docs.multi-scrobbler.app/installation)
## Configuration
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/configuration)
[See the **Configuration** documentation](https://docs.multi-scrobbler.app/configuration)
## Usage
@@ -121,11 +123,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
## Help/FAQ
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ) before creating an issue!
Having issues with connections or configuration? Check the [FAQ](https://docs.multi-scrobbler.app/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/development/dev-common)
[Detailed architecture and development guides for Sources/Clients](https://docs.multi-scrobbler.app/development/dev-common)
## License
+1
View File
@@ -17,6 +17,7 @@
"librariesBlock": ["BadMusic"],
"additionalAllowedLibraryTypes": ["musicvideos"],
"allowMediaTypes": ["audio"],
"frontendUrlOverride": "https://myJellyfinExternal.tld"
},
"options": {
"logPayload": true,
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "Yandex Music",
"type": "ymbridge",
"data": {
"url": "http://192.168.1.161:9980",
"apiKey": "change-me",
"interval": 5
}
}
]
+3 -2
View File
@@ -1,6 +1,7 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
title: FAQ
---
## Connection Issues
@@ -85,7 +86,7 @@ Error: Could not send the specified request to browse. Status code: 401
then YTM has invalidated your authentication.
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#authentication) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
#### When using OAuth Client Authentication
@@ -95,7 +96,7 @@ Refresh your authentication by using the **(Re)authenticate** link from MS's web
The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#authentication) and then restart MS to potentially resolve the problem.
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth) and then restart MS to potentially resolve the problem.
## Configuration Issues
@@ -1,6 +1,7 @@
---
sidebar_position: 2
title: Overview
description: Scrobble Clients
toc_max_heading_level: 4
---
@@ -23,6 +24,71 @@ A **Client** is an application that stores the historical information about what
## Features
### Duplicate Detection
Before a Play is scrobbled to a client MS checks existing scrobbles from the Client's API to see if the Play has already been scrobbled.
For each Play, MS fetches (cached) scrobbles from the Client in a time range inclusive of the Play's listening timestamp and then scores existing scrobbles against the Play based on:
* Similarity of Title, Artists, and Album
* Temporal closeness of the Play's timestamp to the existing scrobble's timestamp
* Whether MS detected the Play as a repeat (applicable to Sources that report realtime Player data)
Detailed scoring breakdowns against each existing scrobble are logged at the `TRACE` level.
<details>
<summary>Detailed Explanation</summary>
A **candidate** (to be scrobbled) Play is first transformed using the configured [`compare.candidate` Hook](/configuration/transforms/#hook), if any exists.
Next, MS checks (up to) the last 100 scrobbles *in-memory* scrobbles that it has made. These are *not* from the Client but the actual scrobbles MS made while it has been running. The data from these scrobbles is much richer than what is usually parsed from the Client which makes it easier to detect duplicates from.
If no in-memory scrobble matches then MS starts comparing the candidate against historical scrobbles fetched from an inclusive time range of the candidate's timestamp.
<h3>Matching Title/Artist/Album</h3>
For these string-based values MS uses an [token-order-invariant](https://github.com/FoxxMD/multi-scrobbler/blob/0dfa4c7aad6df98e13aee6d395827665c2414adb/src/backend/utils/StringUtils.ts#L299) method that scores string similarity based on a (token count) weighted average of two [similarity](https://foxxmd.github.io/string-sameness/#md:strategies) algorithms, [Levenshtien Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) and [Dice's Coefficient](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient). The weighted average ensures that very long strings (Track titles) are scored with a confidence proportional to their length.
<h3>Matching Timestamp</h3>
Timestamps are scored on how temporally close they are. There are four possible scores with decreasing value:
* **Exact** - Timestamps are within 1 second of each other
* **Close** - Timestamps are within `threshold` seconds of each other
* This is determined by the smallest update interval of Source
* Some Sources (subsonic) only update every 60 seconds so this is the smallest "close" value possible. Most are 10 seconds. Signified by `(Needed <10s)` in the breakdown example below.
* **Fuzzy** - One timestamp is within `threshold` seconds of the *end* of the other timestamp
* Sources can set the scrobble timestamp at different times. Some do it when the track is started listening to, some when it ends, some when the *player* stops.
* Where possible, MS knows and keeps track of when this timestamp *should* be, for each Source. If it's not possible then Fuzzy may be allowed.
* **None** - There is no correlation between timestamps
<h3>Scoring and Breakdows</h3>
A candidate Play must score >= 1 to be detected as a duplicate of an existing scrobble.
Each score and a breakdown of the scores for its individual components can be see at the `TRACE` logging level or in the [debug data](/help#copy-play-debug-data) for a scrobble. An example:
```
* Artist: 0.06 * 0.3 = 0.02
* Title: 0.04 * 0.4 = 0.02
* Time: (Exact) 1 * 0.5 = 0.50
* Existing: 19:13:33-04:00 - Candidate: 19:13:33-04:00
* Temporal Sameness: Exact
* Play Diff: 0s (Needed <10s)
* Range Comparison N/A
Score 0.54 => No Match
```
In each component equation, the first number is the similarity (or temporal closeness).
* 0 = no correlation
* 1 = exactly the same
The second number is the *weight* of that component in the final score.
</details>
### Dead Scrobbles
If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up.
@@ -47,7 +47,7 @@ This integration uses an existing, running Discord application to communicate wi
<img src={require('/img/discord_kasm.png').default} height="400"/>
This containerized approach can be used instead of configuring MS to use a [Native Discord](./?discordType=native#dicord-connection) application on a specific machine.
This containerized approach can be used instead of configuring MS to use a [Native Discord](./?discordType=native#discord-connection) application on a specific machine.
**Advantages**
@@ -434,7 +434,7 @@ After [finding your IPC connection path](#find-ipc-connection) you will need to
:::note
Due to the limitations of the Discord local API the other listening activity detection and online status customizations found in [Headless](/configuration/discord/discord-headless#optional-setup) are not available for the Local integration.
Due to the limitations of the Discord local API the other listening activity detection and online status customizations found in [Headless](/configuration/clients/discord/discord-headless#optional-setup) are not available for the Local integration.
:::
@@ -1,6 +1,7 @@
---
sidebar_position: 2
title: Discord
description: Discord Client Overview
toc_max_heading_level: 4
---
@@ -20,7 +20,7 @@ You will need a [Last.fm](https://www.last.fm) account to use the Last.fm API.
After creating your account, [register for an API account here.](https://www.last.fm/api/account/create)
The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callback-redirect-url) section.
The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callbackredirect-url) section.
**Take note of your API Key and Secret after account creation.**
@@ -22,7 +22,7 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
## Configuration
<Config config="ListenbrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
| `LZ_TOKEN` | Yes | | User token from your LZ profile |
@@ -64,7 +64,7 @@ The handle used with multi-scrobbler should be your **full** ATProto handle, inc
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
<Config config="RockskyClientConfig" fileContent={JsonConfig} client name="rocksky">
<Config config="RockSkyClientConfig" fileContent={JsonConfig} client name="rocksky">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
+39 -6
View File
@@ -1,6 +1,7 @@
---
sidebar_position: 2
title: Overview
description: Configuration
toc_max_heading_level: 3
---
import Tabs from '@theme/Tabs';
@@ -168,7 +169,7 @@ Each entry for a Source/Client includes a **Configuration** section that describ
## Secrets Interpolation
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configurations/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configuration/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
Multi-scrobbler will look for patterns in text fields within _all_ of your json files:
@@ -179,7 +180,7 @@ Multi-scrobbler will look for patterns in text fields within _all_ of your json
<summary>Example</summary>
Given this [Last.fm](/configurations/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
Given this [Last.fm](/configuration/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
```json title="lastfm.json"
[
@@ -294,7 +295,7 @@ All of the activities below are **always** cached **in-memory** with an optional
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
If you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
:::
@@ -337,7 +338,7 @@ Authentication sessions/tokens/etc... are cached for quicker requests and for pe
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
If you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
:::
@@ -555,7 +556,7 @@ If you do not need the dashboard and/or ingress sources, or have security concer
:::warning
Any **[ingress-based sources](/configuration/sources#ingress) will be unusable** (Webscrobbler, etc...) if this is disabled.
Any **[ingress-based sources](/configuration/sources?sourceComm=ingress#by-communication-method) will be unusable** (Webscrobbler, etc...) if this is disabled.
:::
@@ -725,5 +726,37 @@ It includes metrics for:
* Count of Queued/Scrobbled/Deadletter scrobbles per [Client](/configuration/clients)
* Number of issues per Source/Client
* If any of these metrics is > 0 it means your Source/Client is not operating normally
* Caching metrics
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
<details>
<summary>Example Usage</summary>
Update your [Prometheus Configuration file](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file) (usually at `/etc/prometheus.yml`) to add a job to the `scrape_configs`[section](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) like this:
```yaml
scrape_configs:
#
# ...your other jobs are here
#
- job_name: multi-scrobbler
scrape_interval: 30s
metrics_path: '/api/metrics'
static_configs:
- targets: ['myMultiscrobblerIp:9078']
```
</details>
<DetailsAdmo status="tip" summary="Grafana Dashboard">
Github user [4rft5](https://github.com/FoxxMD/multi-scrobbler/issues/507#issuecomment-4144982295) created a Grafana dashboard that uses the Prometheus metrics from above.
<img src={require('/img/grafana.png').default} height="800"/>
Download the json dashboard here: [`multiscrobbler-dashboard.json`](/multiscrobbler-dashboard.json)
</DetailsAdmo>
+3 -2
View File
@@ -1,6 +1,7 @@
---
sidebar_position: 3
title: Kitchen Sink
description: Kitchen Sink Example
---
# Example Complex Scenario
@@ -11,13 +12,13 @@ Scenario:
* Each person has their own [Maloja](/configuration/clients/maloja) server
* Each person has their own Spotify account
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you to scrobble from
* You have your own [Youtube Music](/configuration/soures/youtube-mysic) account you want to scrobble from
* You have your own [Youtube Music](/configuration/sources/youtube-music) account you want to scrobble from
* Mary has her own [Last.fm](/configuration/clients/lastfm) account she also wants to scrobble to
* Fred has his own [Spotify](/configuration/sources/spotify) application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
* Fred has a Plex server and wants to scrobble everything he plays
* Mary uses Fred's Plex server but only wants to scrobble her plays from the `podcast` library
* The three of you have a shared library on [Plex](/configuration/sources/plex) called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
* Fred also has his own [Jellyfin server](/configurion/sources/jellyfin) and wants to scrobble everything he plays
* Fred also has his own [Jellyfin server](/configuration/sources/jellyfin) and wants to scrobble everything he plays
* You have an android music app that can scrobble to a [custom listenbrainz server](/configuration/sources/listenbrainz-endpoint)
### All-in-one Config
@@ -49,28 +49,11 @@ If neither is configured automatic scrobble defaults to **off** and you should e
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
<Config config="AzuracastSourceConfig" fileContent={AzuracastConfig} name="azuracast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
| `AZ_URL` | Yes | | Azuracast URL *without station name* |
| `AZ_STATION` | Yes | | The station name shown on the public page |
| `AZURA_LIVE` | No | Yes | Only scrobble when station status is ONLINE |
| `AZURA_LISTENERS_NUM` | No | `true` | Only scrobble if station has any listeners (`true`) or listeners are equal-to/greater-than `X` |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/azuracast.json" language="json5">{AzuracastConfig}</CodeBlock>
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={AzuracastConfig} name="azuracast"/>
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
</TabItem>
</Tabs>
</Config>
+6 -22
View File
@@ -44,28 +44,6 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
</TabItem>
</Tabs>
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ------------------------------------------------ |
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerConfig}</CodeBlock>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={DeezerConfig} name="deezer"/>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
</Tabs>
### Duplicate detection
Third party integrations with Deezer (Sonos) may cause Deezer to [report the same track many times in listening history.](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2922374738) If you experience this issue modify your Deezer Source config (file-based only) to include the option `"fuzzyDiscoveryIgnore": "aggressive"`
@@ -153,7 +131,13 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
:::
</details>
## Configuration
<Config config="DeezerCompatConfig" fileContent={DeezerConfig} name="deezer">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ------------------------------------------------ |
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
</Config>
</TabItem>
<TabItem value="official" label="Official API">
@@ -144,8 +144,7 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
<Config config="ChromecastSourceConfig" fileContent={ChromecastConfig} name="chromecast">
:::note
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
:::
@@ -157,28 +156,4 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
| CC_BLACKLIST_DEVICES | No | | Do not scrobble from these Cast devices. Comma-delimited list |
| CC_WHITELIST_APPS | No | | Only scrobble from these casted Apps. Comma-delimited list. EX spotify, pandora |
| CC_BLACKLIST_APPS | No | | Do not scrobble from these casted Apps. Comma-delimited list |
</TabItem>
<TabItem value="file" label="File">
<details>
<summary>Example</summary>
<CodeBlock title="CONFIG_DIR/chromecast.json" language="json5">{ChromecastConfig}</CodeBlock>
</details>
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<details>
<summary>Example</summary>
<AIOExample data={ChromecastConfig} name="chromecast"/>
</details>
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
</TabItem>
</Tabs>
</Config>
+3 -26
View File
@@ -2,6 +2,7 @@
title: Icecast
toc_min_heading_level: 2
toc_max_heading_level: 5
description: Icecast Source Configuration
---
import Tabs from '@theme/Tabs';
@@ -62,32 +63,8 @@ If no `sources` are configured for multi-scrobbler it will automatically try to
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
<Config config="IcecastSourceConfig" fileContent={IcecastConfig} name="icecast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ----------- |
| `ICECAST_URL` | Yes | | Icecast URL |
</TabItem>
<TabItem value="file" label="File">
<details>
<summary>Example</summary>
<CodeBlock title="CONFIG_DIR/icecast.json" language="json5">{IcecastConfig}</CodeBlock>
</details>
or <SchemaLink lower objectName="IcecastSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<details>
<summary>Example</summary>
<AIOExample data={IcecastConfig} name="icecast"/>
</details>
or <SchemaLink lower objectName="IcecastSourceConfig"/>
</TabItem>
</Tabs>
</Config>
+30 -31
View File
@@ -35,36 +35,35 @@ By default...
</DetailsAdmo>
<DetailsAdmo type="tip" summary="External Asset URL">
If you use MS and Jellyfin on an internal or otherwise inaccessible network but still want album art assets used in the UI or for [Discord](/configuration/clients/discord) to be accessible you can specify a URL to replace your internal url.
Use (file) `frontendUrlOverride` or (env) `JELLYFIN_FRONTEND_URL_OVERRIDE` to replace `url`/`JELLYFIN_URL`, respectively, when rendering links for the dashboard UI or other resources.
Example:
Your `JELLYFIN_URL` is `http://jellyfin:8096` but your Jellyfin instance is externally accessible (by MS or in general) at `https://jellyfin.mydomain.com`.
Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for the MS dashboard to use `https://jellyfin.mydomain.com/art/1234.jpg` when linking to album art.
</DetailsAdmo>
#### Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
| --------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
| `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/jellyfin.json" language="json5">{JellyfinConfig}</CodeBlock>
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={JellyfinConfig} name="jellyfin"/>
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
</TabItem>
</Tabs>
<Config config="JellyApiSourceConfig" fileContent={JellyfinConfig} name="jellyfin">
| Environmental Variable | Required? | Default | Description |
| -------------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
| `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` |
| `JELLYFIN_FRONTEND_URL_OVERRIDE` | No | | The external URL to use for album art assets instead of `JELLYFIN_URL` |
</Config>
+2 -20
View File
@@ -49,28 +49,10 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
<EnvType>
<Config config="JRiverSourceConfig" fileContent={JriverConfig} name="jriver">
| Environmental Variable | Required | Default | Description |
|------------------------|----------|---------------------------------|------------------------------------------------|
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |
</EnvType>
</TabItem>
<TabItem value="file" label="File">
<FileType>
<CodeBlock title="CONFIG_DIR/jriver.json" language="json5">{JriverConfig}</CodeBlock>
or <SchemaLink lower objectName="JRiverSourceConfig"/>
</FileType>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOType>
<AIOExample data={JriverConfig} name="jriver"/>
or <SchemaLink lower objectName="JRiverSourceConfig"/>
</AIOType>
</TabItem>
</Tabs>
</Config>
@@ -1,5 +1,6 @@
---
title: Koito
description: Koito (Source)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -1,5 +1,6 @@
---
title: Last.fm
description: Last.fm (Source)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -1,5 +1,6 @@
---
title: Libre.fm
description: Libre.fm (Source)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -9,7 +9,9 @@ import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import ListenbrainzEndpointConfig from '!!raw-loader!@site/../config/endpointlz.json.example';
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Listenbrainz server.
This Source enables multi-scrobbler to accept scrobbles from outside applications **as if it was a Listenbrainz server.**
If your service/player has a "Scrobble to Listenbrainz" feature and there is an option to set the Listenbrainz URL then multi-scrobbler can accept scrobbles from that service/player by imitating a real Listenbrainz server.
:::tip[Other Uses]
@@ -18,44 +20,91 @@ This Source enables multi-scrobbler to accept scrobbles from outside application
:::
## Setup
### URL
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Listenbrainz scrobbles at
In general, you should use one of the following as the **base URL** when configuring your application to scrobble to multi-scrobbler:
* `http://myMultiScrobblerIP:9078`
* `http://myMultiScrobblerIP:9078/1/`
<DetailsAdmo type="tip" summary="Default Scrobble Submit URL">
If you are not using a [slug for multiple endpoints](#multiple-endpoints) then multi-scrobbler will accept Listenbrainz scrobbles at
```
http://myMultiScrobblerIP:9078/1/submit-listens
```
which is the "standard" Listenbrainz server path for scrobbling.
which is the **standard** Listenbrainz server path for scrobbling.
In general, you should use `http://myMultiScrobblerIP:9078/1/` as the **base path** when configuring your application to scrobble to multi-scrobbler.
</DetailsAdmo>
<details>
<DetailsAdmo type="note" summary="Some Services Require HTTPS">
<summary>URL Setup for Popular Apps</summary>
Some services that can scrobble to a custom Listenbrainz URL may require the URL to have a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
##### Navidrome
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
</details>
Use a slug only if you need to setup multiple Listenbrainz Endpoint sources and cannot use different tokens.
If a slug is used then the URL will be:
```
http://myMultiScrobblerIP:9078/api/listenbrainz/mySlug
```
:::note
Some Listenbrainz applications may require custom Listenbrainz URLs to be a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
:::
</DetailsAdmo>
### Token
Most Listenbrainz clients require a token (Authentication Token) to be provided during setup. This value can be anything you want, just make sure to use the same value for `token` in your multi-scrobbler configuration for the endpoint.
### Multiple Endpoints
If you are setting up multiple [Listenbrainz (Endpoint) Sources](/configuration/sources/listenbrainz-endpoint) MS can differentiate scrobbles based on [Token](#token) so that you can use the same endpoint for many users.
However, if you cannot configure multiple tokens for your scenario you may also use a URL **Slug** to tell MS which Source belongs to a specific user.
<details>
<summary>Using a Slug</summary>
When a **slug** is defined in ENV/File config MS will listen for listenbrainz routes using the slug as a the route path. Example:
```
LZE_SLUG=foobar
```
MS will accept calls to
```
http://myMultiScrobblerIP:9078/api/listenbrainz/foobar
```
for scrobble submission.
</details>
## Setup for Popular Apps
### Navidrome
<details>
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
</details>
### Music Assistant
<details>
[Music Assistant Docs](https://www.music-assistant.io/plugins/listenbrainz_scrobble/)
* In your Music Assistant UI navigate to **Settings** -> **Plugins**
* Click **Add a plugin**
* Add **ListenBrainz Scrobbler**
* In the ListenBrainz Scrobbler configuration screen:
* User Token is the [Token](#token) you configured
* Toggle **Show advanced settings**
* Base URL is the [URL](#url) you can configured, or the equivalent of `http://myMultiScrobblerIP:9078` for your MS instance
* Save the configuration and you are ready to scrobble
To troubleshoot any errors, and assuming you are using Home Assistant, view the Logs in the Music Assistant Add-ons. Please include these logs in any reported issue.
</details>
## Configuration
@@ -1,5 +1,6 @@
---
title: Listenbrainz
description: Listenbrainz (Source)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
+1 -1
View File
@@ -32,7 +32,7 @@ MPRIS communication requires multi-scrobbler to have access to the host's dbus-d
##### Local Unix Socket
The most secure way is to **run a multi-scrobbler container on the same host as MPRIS.** In this scenario you can simply mount the **user session** dbus unix socket into the container. Make sure [`PUID` and `PGID` ENVs are configured](/installation/#linux-host) so the container has permissions to access the socket. Then, modify your compose stack with these additional values:
The most secure way is to **run a multi-scrobbler container on the same host as MPRIS.** In this scenario you can simply mount the **user session** dbus unix socket into the container. Make sure [`PUID` and `PGID` ENVs are configured](/installation?dockerSetting=linuxHost#recommended-settings) so the container has permissions to access the socket. Then, modify your compose stack with these additional values:
```yaml title="docker-compose.yaml"
@@ -1,5 +1,6 @@
---
title: Rocksky
description: Rocksky (Source)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -31,7 +32,7 @@ See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration f
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
<Config config="RockskyClientConfig" fileContent={JsonConfig} name="rocksky">
<Config config="RockskySourceConfig" fileContent={JsonConfig} name="rocksky">
| Environmental Variable | Required? | Default | Description |
| :---------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| `SOURCE_ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
@@ -1,6 +1,7 @@
---
sidebar_position: 2
title: Overview
description: Scrobble Sources
toc_max_heading_level: 4
---
@@ -40,6 +41,7 @@ A **Source** is a data source that contains information about tracks you are pla
| [WebScrobbler](/configuration/sources/webscrobbler) | [Ingress](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [VLC](/configuration/sources/vlc) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Yamaha MusicCast](/configuration/sources/yamaha-musiccast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Yandex Music](/configuration/sources/yandex-music) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | | ✅ | ✅ | ❌ |
| [Youtube Music](/configuration/sources/youtube-music) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
## Features
+18 -1
View File
@@ -16,6 +16,23 @@ If your Spotify player has [Automix](https://community.spotify.com/t5/FAQs/What-
#### Authenticate Spotify with Multi-Scrobbler
<DetailsAdmo type="warning" summary="Premium Account Required">
Starting [March 9th 2026](https://developer.spotify.com/blog/2026-02-06-update-on-developer-access-and-platform-security) all applications on the Spotify Developer platform will only continue to work if they are **owned** by a **Spotify Premium** account.
If your App is not created by a Premium account you will likely recieve a 403 error when trying to use MS with Spotify.
If you do not have a Spotify Premium account *you can still use Spotify with MS* but it will require you to coordinate with a (friend?) user who *does* have Spotify Premium:
* Follow the steps below **logged in as the Spotify Premium account user**
* After completing App creation (step 4 or 5), navigate to the **User Management** page for the created App
* Add **your** (non-premium) account here using your account email/name
* Complete the rest of the authentication/configuration process normally
Assure your friend: **Creating a Spotify Developer app does not give anyone access to the owner's account.** The authentication process within MS is where authorization is given: it requires clicking the link in MS's dashboard and following a separate auth flow. This auth flow only authorizes access to the specific account used in the flow, unrelated to the owner of the developer app.
</DetailsAdmo>
To access your Spotify history you must [create a Spotify App](https://developer.spotify.com/dashboard) to get a
Client ID/Secret.
@@ -48,7 +65,7 @@ Use **one** of the following methods to specify a valid Redirect URI and complet
In this scenario you can use `http://127.0.0.1:9078/callback` as the Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV.
After completing authentication, copy the contents of local multi-scrobbler's [configuration folder](/installation#storage) to the final location of where multi-scrobbler will be run.
After completing authentication, copy the contents of local multi-scrobbler's [configuration folder](/installation?dockerSetting=storage#recommended-settings) to the final location of where multi-scrobbler will be run.
</TabItem>
<TabItem value="echo" label="Echo">
**Use a docker container on the same machine your browser is on to "intercept" the callback URL from Spotify authentication, then manually finish the process by changing the domain to your multi-scrobbler instance.**
@@ -30,7 +30,7 @@ Use the optional `usersAllow` property with **File** or **AIO** configuration to
## Configuration
<Config config="SubsonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
<Config config="SubSonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------|
| `SUBSONIC_USER` | Yes | | |
@@ -1,5 +1,6 @@
---
title: Teal.fm
description: Teal.fm (Source)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -86,7 +86,7 @@ This affects **only** MS's behavior and does not affect the general connector be
## Configuration
<Config config="WebscrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
<Config config="WebScrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------------------------------------------|
| WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs |
@@ -0,0 +1,79 @@
---
title: Yandex Music
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/ymbridge.json.example';
Monitor your [**Yandex Music**](https://music.yandex.com) listening activity in real-time.
This Source requires a third party docker container to communicate with Yandex Music, [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge).
<details>
<summary>Example Docker Compose with `yandex-music-bridge`</summary>
```yaml title="~/msData/docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT
# ...your other sources/clients here
# add for yandex music
// highlight-start
- YMBRIDGE_URL=http://yandex-music-bridge:9980
- YMBRIDGE_API_KEY=change-me
// highlight-end
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
// highlight-start
yandex-music-bridge:
image: ghcr.io/druidblack/yandex-music-bridge:latest
environment:
- TZ=Europe/Moscow
- YM_TOKEN=AgAAAAACO3_345345
- YM_API_KEY=change-me
- YM_PORT=9980
- YM_LANGUAGE=ru
- YM_ENABLE_YNISON=true
- YM_PUSH_TTL=45
- YM_QUEUE_CACHE_TTL=15
- YM_LOG_LEVEL=INFO
ports:
- "9980:9980"
restart: unless-stopped
// highlight-end
```
You will need to acquire your own token for `YM_TOKEN` used in [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge) using the instructions provided there.
</details>
<DetailsAdmo type="tip" summary="Need Help?">
If your issue is specifically related to [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge) (errors in the container, usage instructions, general Yandex setup like token etc...) please open an issue on **that repository** instead of Multi-Scrobbler's.
If you have issues specifically with the MS Source please mention [**Druidblack**](https://github.com/Druidblack) (`@Druidblack`) when opening an [issue](https://github.com/FoxxMD/multi-scrobbler/issues/new/choose) or [discussion](https://github.com/FoxxMD/multi-scrobbler/discussions/new/choose). Yandex Music is [not available](https://en.wikipedia.org/wiki/Yandex_Music) in the country of the main Multi-Scrobbler developer (FoxxMD) so they can only help troubleshooting general MS issues related to Yandex Music.
</DetailsAdmo>
## Configuration
<Config config="YandexMusicBridgeSourceConfig" fileContent={JsonConfig} name="ymbridge">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :------------------------------------------------------------------------ |
| `YMBRIDGE_URL` | Yes | | `http://URL:PORT` for the `yandex-music-bridge` container |
| `YMBRIDGE_API_KEY` | No | | The same key used for `YM_API_KEY` on the `yandex-music-bridge` container |
</Config>
@@ -1,5 +1,6 @@
---
title: Musicbrainz Stage
description: Enhancing Scrobbles with Musicbrainz
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -50,7 +51,7 @@ Set up [Valkey Caching](/configuration?cachedThings=metadata#caching) to cache M
### API Setup
To avoid rate limiting, MusicBrainz requires API users to identify themself using a [User-Agent that contains an email or URL.](https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?) You must set this in the `data` for your [stage configuration](http://localhost:3000/docs/configuration/transforms/#configuring-stages) in your [AIO Config](/configuration?configType=aio#configuration-types):
To avoid rate limiting, MusicBrainz requires API users to identify themself using a [User-Agent that contains an email or URL.](https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?) You must set this in the `data` for your [stage configuration](/configuration/transforms/#configuring-stages) in your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
@@ -139,7 +140,7 @@ Example:
### Rules and Hooks
[Add your Stage](https://foxxmd.github.io/multi-scrobbler/configuration/transforms/#stage) to a Source or Client by specifying it in a [Hook](/configuration/transforms/#hook):
[Add your Stage](/configuration/transforms/#stage) to a Source or Client by specifying it in a [Hook](/configuration/transforms/#hook):
```json5 title="subsonic.json"
[
@@ -160,7 +161,7 @@ Example:
]
```
Each [**Stage Rule**](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition](/configuration/transforms#conditional-moditication):
Each [**Stage Rule**](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition](/configuration/transforms#conditional-modification):
<details>
@@ -247,11 +248,11 @@ To configure [stage defaults](#stage-configuration) use `MB_PRESETS` with a comm
* `fields` - Applies [Field Scoring](#field-scoring) to match sorting so match text is baised towards similarity to original scrobble
* `sensible` - Applies the [Sensible Default](#sensible-default) configuration for [sorting releases](#sorting-releases)
Finally, use ENV `*_TRANSFORMS=musicbrainz` on each Source/Client you wish to apply this stage to. This applies the stage in the [`preTransform` Hook](/configuration/transforms#lifecycle-hooks) with all [Rules](#rules) enabled.
Finally, use ENV `*_TRANSFORMS=musicbrainz` on each Source/Client you wish to apply this stage to. This applies the stage in the [`preTransform` Hook](/configuration/transforms#lifecycle-hooks) with all [Rules](#rules-and-hooks) enabled.
The `*` stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example:
* All [Subsonic Source](http://localhost:3000/docs/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
* All [Subsonic Source](/configuration/sources/subsonic) ENVs look like `SUBSONIC_USER=myuser` etc...
* Use `SUBSONIC_TRANSFORMS=musicbrainz`
<details>
@@ -706,7 +707,7 @@ Sorting can be used instead of filters, or in conjunction with filters, it's you
#### Release Attributes {#sorting-releases}
Each [Release Attribute](#release-attribute) has one additional property that can be used to **rank** releases based on the order of the values you give it. This is the `priority` property.
Each [Release Attribute](#release-attributes) has one additional property that can be used to **rank** releases based on the order of the values you give it. This is the `priority` property.
In configuration:
@@ -777,6 +778,10 @@ In the final sorting of Recordings, title score + artist score is combined with
## Best Practices
### Caching
You **should** setup [metadata caching](/configuration/transforms#caching) to reduce API calls, improve transform performance, and reduce memory usage when using this stage.
### Sensible Default
Generally, the Musicbrainz Stage can be used without any of the [optional configuration](#configuration) and you should still see good results from matches. The top [scored](#score) match is, anecdotally, good enough for correcting and filling in surface-level play data like Title and Artist names.
@@ -818,7 +823,7 @@ Consider adding [Artist Extraction (`artist`)](#search-methods) in **Native Mode
How complete, and accurate, your scrobble data is from a certain Source should dictate what [Search Methods](#search-methods) you should use for matching.
Musicbrainz agressively normalizes **artist, album, and title** fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like [Subsonic](/configuration/sources/subsonic) and [Last.fm](/configuration/sources/lastfm).
Musicbrainz agressively normalizes **artist, album, and title** fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like [Subsonic](/configuration/sources/subsonic) and [Last.fm](/configuration/sources/lastfm-source).
<details>
@@ -869,7 +874,7 @@ When using your own filters consider:
### Using Partial Match
Use [Rules](#rules) to apply MusicBrainz match data selectively.
Use [Rules](#rules-and-hooks) to apply MusicBrainz match data selectively.
If you know that your music collection is well organized and you do not want to change the artists/title/album etc... sent to a [Client](/configuration/clients), you can still benefit from matches by only applying MBIDs using `meta` so that any Client that supports Musicbrainz data ([Koito](/configuration/clients/koito), [Tealfm](/configuration/clients/tealfm), [Listenbrainz](/configuration/clients/listenbrainz), [Rocksky](/configuration/clients/rocksky)) can still get that data.
@@ -1086,7 +1091,7 @@ Your [AIO Config](/configuration?configType=aio#configuration-types):
}
```
In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?configType=file#configuration-types):
In a [Jellyfin](/configuration/sources/jellyfin) [File Config](/configuration?configType=file#configuration-types):
```json5 title="jellyfin.json"
[
@@ -1119,7 +1124,7 @@ In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?co
* [Sensible Defaults](#sensible-default)
* Fallback search with [Artist Extraction](#search-methods)
* [Default Native Stage](/configuration/transforms/native/#default-stage)
* Uses [Flow Control](/configurations/transforms#flow-control) to run Native only if there are no Musicbrainz matches
* Uses [Flow Control](/configuration/transforms#flow-control) to run Native only if there are no Musicbrainz matches
<details>
@@ -1153,7 +1158,7 @@ Your [AIO Config](/configuration?configType=aio#configuration-types):
}
```
In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?configType=file#configuration-types):
In a [Jellyfin](/configuration/sources/jellyfin) [File Config](/configuration?configType=file#configuration-types):
```json5 title="jellyfin.json"
[
@@ -1,5 +1,6 @@
---
title: Native Stage
description: Enhancing Scrobbles with Built-in Parsing
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -75,7 +76,7 @@ Config => extracted artists
### Rules
Each [Rule](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition.](/configuration/transforms#conditional-moditication):
Each [Rule](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition.](/configuration/transforms#conditional-modification):
```json5
{
@@ -96,7 +97,7 @@ If a rule is not present then multi-scrobbler defaults it to `true`.
For your convenience, if you do not define any Native [Stage Configurations](/configuration/transforms#configuring-stages) then multi-scrobbler automatically builds one using the defaults shown in [configuration](#configuration).
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#modification-stage).
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#stage).
<details>
@@ -132,7 +133,7 @@ Use ENV `*_TRANSFORMS=native` on each Source/Client you wish to apply this stage
The `*` stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example
* All [Subsonic Source](http://localhost:3000/docs/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
* All [Subsonic Source](/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
* Use `SUBSONIC_TRANSFORMS=native`
<details>
@@ -36,7 +36,7 @@ Scrobble **Clients** listen for discovered Plays from Sources, then...
* The Client **compares** this Play to all the other scrobbles it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
* The Client **scrobbles** the Play downstream to the scrobble service and adds it as a Scrobble it has seen already
## Lifecyle Hooks
## Lifecycle Hooks
You'll notice there is a pattern above that looks like this:
@@ -376,7 +376,7 @@ Specifying these Rules is **not** the same as [configuring the Stage](#configuri
Stages may be optionally configured to **continue** to the next Stage or **stop** (end early) all subsequent Stages, based on the outcome of the currently running Stage.
These three properties can be added to the [Modification Stage](#modification-stage) data, alongside [Rules](#rules-for-play-data):
These three properties can be added to the [Modification Stage](#stage) data, alongside [Rules](#stage-rules):
* `onSuccess` (default `continue`) - If the Stage successfully finishes processing
* `onFailure` (default `stop`) - If the Stage encounters an error while processing, or otherwise fails to achieve the transformation result
@@ -585,6 +585,47 @@ The output shows the diff between the previous stage (or original Play) and the
</details>
## Best Practices
### Caching
MS uses [caching](/configuration/#caching) to reduce the number of API calls needed for stages like [Musicbrainz](/configuration/transforms/musicbrainz) and to speed up all transforms by caching steps and results. However, the default caching strategy uses a small cache size and very short [TTLs](https://en.wikipedia.org/wiki/Time_to_live) because it is *in-memory*.
**If you are using any Transform stages you should configure [secondary caching with Valkey for Metadata](/configuration/?cacheType=valkey&cachedThings=metadata#secondary-caching-configuration)** to increase the cache size and lifetime of cached items. This will also reduce memory usage in MS.
<details>
<summary>Example</summary>
Add valkey service to your [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker) and configure MS to use it for metadata:
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
# ...
environment:
# ...
// highlight-start
- CACHE_METADATA=valkey
- CACHE_METADATA_CONN=redis://valkey:6379
// highlight-end
# ...
// highlight-start
valkey:
image: valkey/valkey
volumes:
- valkeydata:/data
volumes:
valkeydata:
driver: local
// highlight-end
```
</details>
## Examples
See **Examples** sections in specific Stage docs (also in the sidebar):
@@ -1,5 +1,6 @@
---
title: User Stage
description: Enhancing Scrobbles with Regex Parsing
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -16,7 +17,7 @@ The user [Stage `type`](/configuration/transforms#stage) is `user`.
## Configuration
All [Stage Configuration](/configuration/transforms#configuring-stage) is done using [**search-and-replace** expressions](#search-and-replace-expression) inside individual [rules](#rules). Default configuration for all Rules can still be done using Stage `defaults`.
All [Stage Configuration](/configuration/transforms#configuring-stages) is done using [**search-and-replace** expressions](#search-and-replace-expression) inside individual [rules](#rules). Default configuration for all Rules can still be done using Stage `defaults`.
<details>
@@ -46,9 +47,9 @@ All [Stage Configuration](/configuration/transforms#configuring-stage) is done u
#### Default Stage
For your convenience, if you do not define any User [Stage Configurations](/configuration/transforms#configuration) then multi-scrobbler automatically builds an empty one for you.
For your convenience, if you do not define any User [Stage Configurations](/configuration/transforms#configuring-stages) then multi-scrobbler automatically builds an empty one for you.
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#modification-stage).
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#stage).
<details>
@@ -126,7 +127,7 @@ The `replace` property uses javascript's [`replace()` function and so can use an
### Rules
Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-and-replace expressions](#search-and-replace-expressions):
Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-and-replace expressions](#search-and-replace-expression):
```json5 title="lastfm.json"
[
-8
View File
@@ -1,8 +0,0 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 3
title: Client Development/Tutorial
---
To do...
+2 -2
View File
@@ -3,7 +3,7 @@ toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 1
title: Common Development
description: Start here for MS development
description: Common Development Knowledge
---
# Development
@@ -177,7 +177,7 @@ For more refer to the TS documentation for `PlayObject` or [`AmbPlayObject`](htt
## Creating Clients and Sources
* [Source Development and Tutorial](dev-source.md)
* [Client Development and Tutorial](dev-client.md)
* Client Development and Tutorial (TBD)
## Profiling
+1
View File
@@ -3,6 +3,7 @@ toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 2
title: Source Development/Tutorial
description: Developing a new Source
---
This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review [Common Development](dev-common.md#common-development).
+1 -1
View File
@@ -3,7 +3,7 @@ toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 4
title: Flatpak
description: Building Flatpak App locally
description: Building Flatpak App
---
## Install Requirements
+2 -2
View File
@@ -1,5 +1,5 @@
---
title: 'Help'
title: Help
---
## Getting Useful Data
@@ -54,7 +54,7 @@ Thank you for your time.
### FAQ
Check the [FAQ](/faq) to see if your issue is a known issue or a commonly-experienced issue with Multi-Scrobbler. The FAQ also includes fixes or ways to help troubleshoot these common issues.
Check the [FAQ](/FAQ) to see if your issue is a known issue or a commonly-experienced issue with Multi-Scrobbler. The FAQ also includes fixes or ways to help troubleshoot these common issues.
### Existing Issues
+2
View File
@@ -31,6 +31,7 @@ A dockerized app that monitors your music listening activity from *everywhere* a
* [MPD (Music Player Daemon)](/configuration/sources/mpd)
* [MPRIS (Linux Desktop)](/configuration/sources/mpris)
* [Musikcube](/configuration/sources/musikcube)
* [Music Assistant](/configuration/sources/listenbrainz-endpoint#setup-for-popular-apps)
* [Plex](/configuration/sources/plex)
* [Rocksky](/configuration/sources/rocksky-source)
* [Sonos](/configuration/sources/sonos)
@@ -40,6 +41,7 @@ A dockerized app that monitors your music listening activity from *everywhere* a
* [WebScrobbler](/configuration/sources/webscrobbler)
* [VLC](/configuration/sources/vlc)
* [Yamaha MusicCast](/configuration/sources/yamaha-musiccast)
* [Yandex Music](/configuration/sources/yandex-music)
* [Youtube Music](/configuration/sources/youtube-music)
* Supports scrobbling to many [**Clients**](/configuration/clients)
* [Discord](/configuration/clients/discord) (Now Playing)
+131 -152
View File
@@ -1,6 +1,7 @@
---
sidebar_position: 1
title: 'Overview'
title: Overview
description: Installation
---
import Tabs from '@theme/Tabs';
@@ -37,81 +38,39 @@ Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
:::
Or use the example `docker-compose.yml`, found in the repo, after modifying it to fit your configuration:
### Recommended Settings
<details>
<Tabs groupId="dockerSetting" queryString>
<TabItem value="storage" label="Storage">
You **should** bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
<summary>`docker-compose.yml`</summary>
<CodeBlock language="yaml">{ComposeStack}</CodeBlock>
</details>
Recommended configuration steps for docker or docker-compose usage:
#### Storage
You should bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
<details>
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
[Using `-v` method for docker](https://docs.docker.com/storage/bind-mounts/#start-a-container-with-a-bind-mount):
```shell
docker run ... -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose" default>
[Using docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
volumes:
- "./config:/config"
```
</TabItem>
</Tabs>
</details>
#### Networking
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
// highlight-start
volumes:
- "./config:/config"
// highlight-end
```
</TabItem>
<TabItem value="networking" label="Networking">
If you are using a [bridge network](https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/) (default docker setup) you must map a port to the container in order to access the dashboard and use MS with some sources (Webscrobbler, LFM/LZ Endpoints). The default container port is `9078`.
<details>
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
// highlight-start
ports:
- "9078:9078"
// highlight-end
```
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
[Docker `run` publish options](https://docs.docker.com/engine/reference/commandline/run/#publish):
```shell
docker run ... -p 9078:9078 foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose" default>
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
ports:
- "9078:9078"
```
</TabItem>
</Tabs>
</details>
##### Base URL
Optionally, when
</TabItem>
<TabItem value="baseUrl" label="Base URL">
**Optionally**, when
* using a [Source or Client](../configuration/configuration.mdx) that has a "Redirect URI" that you have not explicitly defined
* and
@@ -120,63 +79,89 @@ Optionally, when
set the [Base URL](../configuration/configuration.mdx#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
<details>
<summary>Example</summary>
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
environment:
// highlight-start
- BASE_URL=http://hostMachineIP
// highlight-end
```
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
```shell
docker run ... -e BASE_URL="http://hostMachineIP" foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose" default>
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
</TabItem>
<TabItem value="tz" label="Timezone">
Set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ`.
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
environment:
- BASE_URL=http://hostMachineIP
```
</TabItem>
</Tabs>
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
environment:
# Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
// highlight-start
- TZ=America/New_York
// highlight-end
```
</details>
#### Other
* (Optionally) set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ` ([docker](https://docs.docker.com/engine/reference/commandline/run/#env)) ([docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#environment))
#### Linux Host
::::info
If you are running this container with **Docker** on a **Linux Host** you must specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
</TabItem>
<TabItem value="linuxHost" label="Linux Host">
If you are running this container with **Docker** on a **Linux Host** you **should** specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
To get the UID and GID for the current user run these commands from a terminal:
* `id -u` -- prints UID
* `id -g` -- prints GID
::::
#### Read Only
</TabItem>
<TabItem value="ro" label="Read Only">
MS can be hardended by setting the file system as read only with [`read_only`](https://docs.docker.com/reference/compose-file/services/#read_only), with a few exceptions:
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
# ...
read_only: true
tmpfs:
- /run:exec
- /tmp:noexec
multi-scrobbler:
image: foxxmd/multi-scrobbler
# ...
// highlight-start
read_only: true
tmpfs:
- /run:exec
- /tmp:noexec
// highlight-end
```
</TabItem>
<TabItem value="caching" label="Caching">
**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration/?cacheType=valkey#secondary-caching-configuration) to take advantage of faster performance and reduced memory usage.
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
# ...
environment:
# ...
// highlight-start
CACHE_METADATA=valkey
CACHE_METADATA_CONN=redis://valkey:6379
// highlight-end
# ...
// highlight-start
valkey:
image: valkey/valkey
volumes:
- valkeydata:/data
volumes:
valkeydata:
driver: local
// highlight-end
```
</TabItem>
</Tabs>
### Docker Usage Example
:::tip
@@ -193,49 +178,43 @@ The example scenario:
* Docker container located on a different IP (`192.168.0.100`) so use [Base URL](/configuration#base-url)
* Config/data directory on host machine in a directory next to `docker-compose.yml`
* Linux uid/gid is `1000:1000`
* Optional caching using valkey (after uncommenting)
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
```bash
docker run --name multi-scrobbler -e "JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c" -e "JELLYFIN_URL=192.168.0.101:8096" -e "JELLYFIN_USER=MyUser" -e "BASE_URL=192.168.0.100" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /home/myUser/ms:/config foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="file" label="File">
```bash
docker run --name multi-scrobbler -e "PUID=1000" -e "PGID=1000" -e "BASE_URL=192.168.0.100" -p 9078:9078 -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="docker-compose" label="Docker Compose" default>
See [`docker-compose.yml`](#docker) sample above for more options and annotations.
See [`docker-compose.yml`](#docker) sample above for more options and annotations.
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c
- JELLYFIN_URL=192.168.0.101:8096
- JELLYFIN_USER=MyUser
- BASE_URL=http://192.168.0.100:9078
- MALOJA_URL=http://domain.tld:42010
- MALOJA_API_KEY=1234
- PUID=1000
- PGID=1000
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
</TabItem>
</Tabs>
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c
- JELLYFIN_URL=192.168.0.101:8096
- JELLYFIN_USER=MyUser
- BASE_URL=http://192.168.0.100:9078
- MALOJA_URL=http://domain.tld:42010
- MALOJA_API_KEY=1234
- PUID=1000
- PGID=1000
# uncomment along with valkey service/volume below for better caching
#- CACHE_METADATA=valkey
#- CACHE_METADATA_CONN=redis://valkey:6379
volumes:
- ./config:/config
ports:
- 9078:9078
restart: unless-stopped
#valkey:
# image: valkey/valkey
# volumes:
# - valkeydata:/data
#volumes:
# valkeydata:
# driver: local
```
## Local Installation
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
+2 -1
View File
@@ -1,6 +1,7 @@
---
sidebar_position: 2
title: 'As a Service'
title: As a Service
description: Local Install as a Service
---
If you have multi-scrobbler installed [locally](installation.mdx#nodejs) you can enable it to run as a background service when you login.
+5 -4
View File
@@ -1,5 +1,5 @@
---
title: 'Quickstart'
title: Quickstart
sidebar_position: 2
---
@@ -10,7 +10,7 @@ This guide will get you up and running with multi-scrobbler using [Docker](https
* the dashboard served on port `9078` of a host machine which has an IP of `192.168.0.100`
* data saved to the same directory as the `docker-compose.yml` file
* multi-scrobbler monitoring [Spotify](/configuration/sources/spotify), [Jellyfin](/configuration/sources/jellyfin), Navidrome, [Plex](/configuration/sources/plex), and/or [Last.fm](/configurations/sources/lastfm) for listening activity
* multi-scrobbler monitoring [Spotify](/configuration/sources/spotify), [Jellyfin](/configuration/sources/jellyfin), Navidrome, [Plex](/configuration/sources/plex), and/or [Last.fm](/configuration/sources/lastfm-source) for listening activity
* multi-scrobbler scrobbling to [Lastfm](/configuration/clients/lastfm), [Maloja](/configuration/clients/maloja), and/or [Koito](/configuration/clients/koito)
:::note
@@ -166,7 +166,7 @@ Add these value to the `environment` section in the [`docker-compose.yml` you cr
- MB_PRESETS=default,sensible,native
```
**Presets** are found [here](http://localhost:3000/docs/configuration/transforms/musicbrainz/#env-configuration).
**Presets** are found [here](/configuration/transforms/musicbrainz/#env-configuration).
<h3>Enable Musicbrainz For A Source</h3>
@@ -314,7 +314,7 @@ Modify the `lastfm.json` file you created in the [Setup Sources](#setup-sources)
:::tip
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](/installation#linux-host)
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](/installation?dockerSetting=linuxHost#recommended-settings)
:::
@@ -336,6 +336,7 @@ Visit `http://192.168.0.100:9078` to see the dashboard where
## Next Steps
* See more advanced docker options as well as other install methods in the [**Installation**](/installation#docker) docs
* Setup [secondary caching](/configuration/?cacheType=valkey#secondary-caching-configuration) with [valkey](/installation?dockerSetting=caching#recommended-settings) for increased performance and reduced memory usage
* Review the [**Configuration**](/configuration) docs
* Learn about how to configure multi-scrobbler using files for more complicated Source/Client scenarios
* See all available [**Sources**](/configuration/sources) and [**Clients**](/configuration/clients) alongside configuration examples
+1
View File
@@ -1,6 +1,7 @@
---
sidebar_position: 1
title: 'Overview'
description: Updating
---
import Tabs from '@theme/Tabs';
+65 -30
View File
@@ -1,8 +1,21 @@
import type * as Preset from '@docusaurus/preset-classic';
import * as Preset from '@docusaurus/preset-classic';
import type { Config } from '@docusaurus/types';
import * as themes from 'prism-react-renderer';
import * as Renderers from './lib/socialCard/ImageRenderers.js';
//import sidebars from './sidebars';
let baseSite = 'https://foxxmd.github.io';
// https://docs.netlify.com/build/configure-builds/environment-variables/#deploy-urls-and-metadata
const netlifyDeploy = process.env.DEPLOY_PRIME_URL;
if(netlifyDeploy !== undefined && netlifyDeploy !== null && netlifyDeploy.trim() !== '') {
baseSite = netlifyDeploy;
} else {
const baseSiteEnv = process.env.BASE_SITE;
if(baseSiteEnv !== undefined && baseSiteEnv !== null && baseSiteEnv.trim() !== '') {
baseSite = baseSiteEnv;
}
}
const config: Config = {
future: {
v4: {
@@ -19,7 +32,7 @@ const config: Config = {
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'https://foxxmd.github.io',
url: baseSite,
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: process.env.DOCS_BASE !== undefined && process.env.DOCS_BASE !== '' ? process.env.DOCS_BASE : '/docs',
@@ -31,8 +44,12 @@ const config: Config = {
trailingSlash: true,
onBrokenLinks: 'warn',
onBrokenMarkdownLinks: 'warn',
markdown: {
hooks: {
onBrokenMarkdownLinks: 'warn',
}
},
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
@@ -71,7 +88,7 @@ const config: Config = {
],
],
themes: [
"docusaurus-json-schema-plugin",
//"docusaurus-json-schema-plugin",
[
"@easyops-cn/docusaurus-search-local",
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
@@ -90,6 +107,7 @@ const config: Config = {
'docusaurus-theme-github-codeblock'
],
plugins: [
'docusaurus-plugin-sass',
[
'@docusaurus/plugin-client-redirects',
{
@@ -121,6 +139,16 @@ const config: Config = {
},
]
}
],
[
'@bony_chops/docusaurus-og',
{
path: './preview-images', // relative to the build directory
imageRenderers: {
'docusaurus-plugin-content-docs': Renderers.docs,
'docusaurus-plugin-content-pages': Renderers.docs,
},
},
]
],
themeConfig:
@@ -153,14 +181,14 @@ const config: Config = {
position: 'right',
},
{
href: 'https://foxxmd.github.io/multi-scrobbler/',
href: 'https://multi-scrobbler.app',
label: 'Website',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Docs',
@@ -192,7 +220,7 @@ const config: Config = {
},
{
label: 'Website',
href: 'https://foxxmd.github.io/multi-scrobbler/',
href: 'https://multi-scrobbler.app',
},
],
},
@@ -215,28 +243,22 @@ const config: Config = {
showRunmeLink: false,
runmeLinkLabel: 'Checkout via Runme'
}
} satisfies Preset.ThemeConfig,
headTags: [
{
tagName: 'meta',
attributes: {
name: 'google-site-verification',
content: process.env.GSITEVERIFICATION ?? 'none'
}
}
]
} satisfies Preset.ThemeConfig
};
if (process.env.ANALYTICS !== undefined && process.env.ANALYTICS !== '') {
const script = {
src: process.env.ANALYTICS,
defer: true
}
if (process.env.ANALYTICS_DOMAIN !== undefined && process.env.ANALYTICS_DOMAIN !== '') {
script['data-domain'] = process.env.ANALYTICS_DOMAIN;
}
config.scripts.push(script)
const gverify = process.env.GSITEVERIFICATION;
if(gverify !== undefined && gverify !== null && gverify.trim() !== '') {
config.headTags = [
{
tagName: 'meta',
attributes: {
name: 'google-site-verification',
content: gverify
}
}
]
}
if (process.env.RY_ANALYTICS !== undefined && process.env.RY_ANALYTICS !== '') {
const script = {
src: process.env.RY_ANALYTICS,
@@ -245,10 +267,23 @@ if (process.env.RY_ANALYTICS !== undefined && process.env.RY_ANALYTICS !== '') {
if (process.env.RY_ANALYTICS_SITEID !== undefined && process.env.RY_ANALYTICS_SITEID !== '') {
script['data-site-id'] = process.env.RY_ANALYTICS_SITEID;
}
if (process.env.RY_ANALYTICS_REPLAY !== undefined && process.env.RY_ANALYTICS_REPLAY !== '') {
script['data-session-replay'] = process.env.RY_ANALYTICS_REPLAY;
}
config.scripts.push(script)
}
if(process.env.NETLIFY === 'true') {
// @ts-ignore
config.themeConfig.footer.links.push({
title: 'Docs Powered By',
items: [
{
html: `
<a href="https://www.netlify.com">
<img src="https://www.netlify.com/assets/badges/netlify-badge-color-accent.svg" alt="Deploys by Netlify" />
</a>
`
}
]
});
}
export default config;
+5535 -1733
View File
File diff suppressed because it is too large Load Diff
+31 -15
View File
@@ -1,40 +1,51 @@
{
"name": "my-website",
"version": "0.0.0",
"name": "multi-scrobbler-docs",
"version": "0.12.1",
"private": true,
"scripts": {
"start": "docusaurus start --host 0.0.0.0",
"build": "docusaurus build",
"build": "npm run -s prebuild && docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
"typecheck": "tsc",
"postinstall": "patch-package",
"previews:build-renderer": "tsc --project tsconfig.client.json || exit 0",
"prebuild": "npm run -s previews:build-renderer"
},
"dependencies": {
"@docusaurus/core": "3.8.1",
"@docusaurus/faster": "3.8.1",
"@docusaurus/plugin-client-redirects": "^3.8.1",
"@docusaurus/preset-classic": "3.8.1",
"@bony_chops/docusaurus-og": "^1.0.3-beta.3",
"@docusaurus/core": "3.9.2",
"@docusaurus/faster": "3.9.2",
"@docusaurus/plugin-client-redirects": "^3.9.2",
"@docusaurus/preset-classic": "3.9.2",
"@easyops-cn/docusaurus-search-local": "0.51.1",
"@mdx-js/react": "^3.0.0",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"cf-json-schema-viz": "^0.3.0",
"clsx": "^2.0.0",
"docusaurus-json-schema-plugin": "^1.14.0",
"docusaurus-plugin-sass": "^0.2.6",
"docusaurus-theme-github-codeblock": "^2.0.2",
"json5": "^2.2.3",
"micromark-extension-directive": "^3.0.1",
"modern-react-json-editor": "^1.0.2",
"patch-package": "^8.0.1",
"prism-react-renderer": "^2.3.0",
"raw-loader": "^4.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"sass": "^1.97.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.6.3",
"@docusaurus/tsconfig": "^3.6.3",
"@docusaurus/types": "^3.6.3",
"@types/react": "^18.2.29",
"@docusaurus/module-type-aliases": "^3.9.2",
"@docusaurus/tsconfig": "^3.9.2",
"@docusaurus/types": "^3.9.2",
"@types/react": "^19.2.4",
"typescript": "~5.6.2"
},
"browserslist": {
@@ -50,6 +61,11 @@
]
},
"engines": {
"node": ">=18.0"
"node": ">=24.14.0",
"npm": ">=11.12.1"
},
"overrides": {
"lodash": "4.18.1",
"satori": "0.25.0"
}
}
@@ -0,0 +1,42 @@
diff --git a/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js b/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js
index b308c02..e0d291a 100644
--- a/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js
+++ b/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js
@@ -93,7 +93,16 @@ class DocsPlugin {
await document.write();
}
};
- getHtmlPath = (doc) => doc.metadata?.permalink &&
- path.join(this.context.outDir, doc.metadata.permalink, 'index.html');
+ getHtmlPath = (doc) => {
+ if(doc.metadata?.permalink !== undefined) {
+ let relativePerma = doc.metadata.permalink;
+ if(this.context.baseUrl !== undefined && this.context.baseUrl !== '/') {
+ relativePerma = path.join('/', relativePerma.replace(this.context.baseUrl, ''));
+ }
+ const p = path.join(this.context.outDir, relativePerma, 'index.html');
+ return p;
+ }
+ return undefined;
+ }
}
exports.DocsPlugin = DocsPlugin;
diff --git a/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js b/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js
index 4676f56..03f03a5 100644
--- a/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js
+++ b/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js
@@ -101,6 +101,13 @@ class PagesPlugin {
await document.write();
}
};
- getHtmlPath = (permalink) => path.join(this.context.outDir, permalink, 'index.html');
+ getHtmlPath = (permalink) => {
+ let relativePerma = permalink;
+ if(this.context.baseUrl !== undefined && this.context.baseUrl !== '/') {
+ relativePerma = path.join('/', relativePerma.replace(this.context.baseUrl, ''));
+ }
+ const p = path.join(this.context.outDir, relativePerma, 'index.html');
+ return p;
+ }
}
exports.PagesPlugin = PagesPlugin;
File diff suppressed because one or more lines are too long
+86
View File
@@ -0,0 +1,86 @@
import React, { ReactNode, CSSProperties } from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
// Define the Button type to control the props that can be passed to the Button component.
export type ButtonProps = {
// The size prop can be one of the following values: 'sm', 'lg', 'small', 'medium', 'large', or null.
// We'll convert 'small' to 'sm' and 'large' to 'lg' in the component. 'medium' will be considered null.
size?: 'sm' | 'lg' | 'small' | 'medium' | 'large' | null;
// The outline prop is a boolean that determines if the button should be an outline button.
outline?: boolean;
// The variant prop is a string that determines the color of the button.
// It can be one of the following values: 'primary', 'secondary', 'danger', 'warning', 'success', 'info', 'link', or any other string.
// The default value is 'primary'.
variant: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'link' | string;
// The block prop is a boolean that determines if the button should be a block-level button.
block?: boolean;
// The disabled prop is a boolean that determines if the button should be disabled.
disabled?: boolean;
// The className prop is a string that allows you to add custom classes to the button.
className?: string;
// The style prop is an object that allows you to add custom styles to the button.
style?: CSSProperties;
// The link prop is a string that determines the URL the button should link to.
link: string;
// The label prop is a string that determines the text of the button.
label: string;
onClick?: () => any
}
// Button component that accepts the specified props.
export default function Button ({
size = null,
outline = false,
variant = 'primary',
block = false,
disabled = false,
className,
style = {},
link,
label,
onClick = () => null,
...rest
}: ButtonProps) {
// Map the size prop values to corresponding CSS classes.
const sizeMap = {
sm: 'sm',
small: 'sm',
lg: 'lg',
large: 'lg',
medium: null,
};
const buttonSize = size ? sizeMap[size] : '';
const sizeClass = buttonSize ? `button--${buttonSize}` : '';
const outlineClass = outline ? 'button--outline' : '';
const variantClass = variant ? `button--${variant}` : '';
const blockClass = block ? 'button--block' : '';
const disabledClass = disabled ? 'disabled' : '';
// If the button is disabled, set the destination to null.
const destination = disabled ? null : link;
return (
<Link to={destination}>
<button
className={clsx(
'button',
sizeClass,
outlineClass,
variantClass,
blockClass,
disabledClass,
className
)}
style={style}
role='button'
aria-disabled={disabled}
onClick={(e) => {
e.preventDefault();
onClick()
}}
{...rest}
>
{label}
</button>
</Link>
);
}
+82
View File
@@ -0,0 +1,82 @@
import React, { ReactNode, CSSProperties } from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import Button, { ButtonProps } from './Button';
const leftButton = {
borderRadius: 'var(--ifm-button-border-radius) 0 0 var(--ifm-button-border-radius)'
}
const rightButton = {
borderRadius: '0 var(--ifm-button-border-radius) var(--ifm-button-border-radius) 0'
}
const middleButton = {
borderRadius: '0'
}
// Define the Button type to control the props that can be passed to the Button component.
type ButtonGroup = {
options: [string, string][]
value?: string
defaultValue?: string
// The size prop can be one of the following values: 'sm', 'lg', 'small', 'medium', 'large', or null.
// We'll convert 'small' to 'sm' and 'large' to 'lg' in the component. 'medium' will be considered null.
size?: 'sm' | 'lg' | 'small' | 'medium' | 'large' | null;
variant: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'link' | string;
// The block prop is a boolean that determines if the button should be a block-level button.
block?: boolean;
// The disabled prop is a boolean that determines if the button should be disabled.
disabled?: boolean;
// The className prop is a string that allows you to add custom classes to the button.
className?: string;
// The style prop is an object that allows you to add custom styles to the button.
style?: CSSProperties;
onChange?: (val: string) => any
}
// Button component that accepts the specified props.
export default function ButtonGroup ({
size = null,
variant = 'primary',
block = false,
disabled = false,
className,
style,
options,
value,
defaultValue,
onChange = () => null
}: ButtonGroup) {
if(options === undefined || options.length === 0) {
return null;
}
const buttons = options.map((opt, index) => {
const props: ButtonProps = {
style: middleButton,
outline: true,
size,
variant,
label: opt[1],
link: '#',
onClick: () => onChange(opt[0])
};
if(index === 0) {
props.style = leftButton;
} else if(index === options.length - 1) {
props.style = rightButton;
}
if((value ?? defaultValue) === opt[0]) {
props.outline = false;
}
return <Button key={index} {...props}/>
});
return <span>{buttons}</span>;
}
@@ -0,0 +1,74 @@
import React, { Fragment, useMemo } from "react"
import CodeBlock, {Props as CodeBlockProps} from '@theme/CodeBlock';
import Admonition from '@theme/Admonition';
import ErrorBoundary from "@docusaurus/ErrorBoundary"
import Error from "@theme/Error"
import { Simulate } from "react-dom/test-utils";
import error = Simulate.error;
import json5 from 'json5';
import FileExample, { FileProps } from "./FileExample";
export interface FileConfigProps extends FileProps {
data: string
name: string
configureAs?: 'client' | 'source'
aio?: boolean
filePathName?: string
}
const FileConfigExample = (props: FileConfigProps) => {
const {
data,
configureAs = 'source',
aio = false,
name,
...rest
} = props;
let configObj;
// eslint-disable-next-line prefer-const
try {
configObj = json5.parse(data) as object[];
} catch (e) {
console.error(e);
return <Admonition type="danger" title="Unexpected Error">
<p>Example component crashed because of error!</p>
<CodeBlock>{e.message}</CodeBlock>
</Admonition>
}
const transformed = useMemo(() => {
let content: object;
if(typeof data === 'string') {
content = json5.parse(data);
} else {
content = data;
}
if(configureAs !== undefined && Array.isArray(content)) {
const filtered = content.filter(x => x.configureAs === undefined || x.configureAs === configureAs);
if(aio) {
const configType = configureAs === 'client' ? 'clients' : 'sources';
content = {[configType]: filtered.map(x => ({...x, type: name}))}
} else {
content = filtered;
}
}
return content;
},[data, configureAs, aio, name])
configObj = configObj.filter(x => x.configureAs === undefined || x.configureAs === configureAs);
return <FileExample data={transformed} {...rest}/>
}
const WrappedFileConfigExample = (props: FileConfigProps) => {
return <ErrorBoundary
fallback={({error}) => (
<div>
<p>Example component crashed because of error: {error.message}.</p>
</div>
)}
><FileConfigExample {...props} /></ErrorBoundary>
}
export default WrappedFileConfigExample;
+66 -20
View File
@@ -1,44 +1,90 @@
import React, { Fragment } from "react"
import CodeBlock, {Props as CodeBlockProps} from '@theme/CodeBlock';
import React, { Fragment, useMemo } from "react"
import CodeBlock, { Props as CodeBlockProps } from '@theme/CodeBlock';
import Admonition from '@theme/Admonition';
import ErrorBoundary from "@docusaurus/ErrorBoundary"
import Error from "@theme/Error"
import { Simulate } from "react-dom/test-utils";
import error = Simulate.error;
import json5 from 'json5';
import { useTypedLocalStorage } from "./useLocalStorage";
import ButtonGroup from "./ButtonGroup";
import EditorFetch from '@site/src/components/Schema/SchemaEditorFetch';
import {useWindowSize} from '@docusaurus/theme-common';
export interface FileProps extends Omit<CodeBlockProps, 'children'> {
data: string
client?: boolean
data: string | object
filePathName?: string
schemaName?: string
interactive?: boolean
defaultDisplay?: 'plain' | 'interactive'
readOnlyAtWidth?: number
}
const FileExample = (props: FileProps) => {
const {
data,
client = false,
interactive = true,
defaultDisplay = 'interactive',
schemaName,
filePathName,
readOnlyAtWidth = 1,
...rest
} = props;
let configObj;
// eslint-disable-next-line prefer-const
try {
configObj = json5.parse(data) as object[];
} catch (e) {
console.error(e);
return <Admonition type="danger" title="Unexpected Error">
<p>Example component crashed because of error!</p>
<CodeBlock>{e.message}</CodeBlock>
</Admonition>
const [fileExampleShow, setFileExampleShow] = useTypedLocalStorage('docusaurus.tab.fileExampleShow', defaultDisplay, false);
const windowSize = useWindowSize({desktopBreakpoint: readOnlyAtWidth});
let displaySwitch = null;
const isInteractive = interactive && windowSize === 'desktop'
if (isInteractive) {
displaySwitch = (
<ButtonGroup
options={[['plain', 'Plain'], ['interactive', 'Interactive']]}
defaultValue={defaultDisplay}
value={fileExampleShow}
variant="primary"
size="medium"
onChange={(val) => setFileExampleShow(val as 'interactive' | 'plain')}
/>
);
}
configObj = configObj.filter(x => x.configureAs === undefined || x.configureAs === (client ? 'client' : 'source'));
let fileNameContent = null;
if(filePathName !== undefined && (isInteractive && fileExampleShow === 'interactive')) {
fileNameContent = <code style={{float: 'right', paddingRight: '0.5em', paddingLeft: '0.5em'}}>{filePathName}</code>
}
return <CodeBlock {...rest} language="json5">{JSON.stringify(configObj, null, 2)}</CodeBlock>
let header = null;
if(displaySwitch !== null || fileNameContent !== null) {
header = (
<div style={{marginBottom: '1em'}}>
{displaySwitch}{fileNameContent}
</div>
);
}
const codeBlockContent = useMemo(() =>{
if(isInteractive && fileExampleShow === 'interactive') {
return null;
}
if(typeof data !== 'string') {
return JSON.stringify(data, null, 2);
}
return data;
}, [interactive, fileExampleShow, data]);
return (<Fragment>
{header}
{!isInteractive || fileExampleShow === 'plain' ? <CodeBlock title={filePathName} language="json5">{codeBlockContent}</CodeBlock> : null}
{isInteractive && fileExampleShow === 'interactive' ? <EditorFetch configContent={data} schemaName={schemaName}/> : null}
</Fragment>
);
}
const WrappedFileExample = (props: FileProps) => {
return <ErrorBoundary
fallback={({error}) => (
fallback={({ error }) => (
<div>
<p>Example component crashed because of error: {error.message}.</p>
</div>
@@ -1,6 +1,11 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import ExplorerFetch from '@site/src/components/Schema/ExplorerFetch';
import EditorFetch from '@site/src/components/Schema/SchemaEditorFetch';
import './GenericConfiguration.scss';
import FileConfigExample from './FileConfigExample';
import {useWindowSize} from '@docusaurus/theme-common';
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
@@ -10,19 +15,96 @@ import CodeBlock from '@theme/CodeBlock';
</TabItem>
<TabItem value="file" label="File">
<FileType>
<div
className="schemaWrapper"
style={{
display: "flex"
}}
>
<div style={{ padding: '0 1em 0 1em', flexBasis: 'min-content', flexGrow: 2}}>
<DetailsAdmo type="note" summary="Config Structure">
This displays the **structure** of the [File Configuration](/configuration?configType=aio#configuration-types) for a <strong children={props.name}/> type <strong children={props.client ? 'Client' : 'Source'}/> with all possible properties, their shape, and descriptions/types. Use this to understand how to write a valid config.
</DetailsAdmo>
<ExplorerFetch name={`schemas/${props.config}`} />
</div>
<div style={{ flexBasis: 'auto', flexGrow: 3 }}>
<div className="fileEditor">
<DetailsAdmo type="note" summary="Config Example">
This displays an **example config file** of a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> [File Configuration](/configuration?configType=aio#configuration-types) that adheres to the shown **Config Structure**.
<FileExample title={`CONFIG/${props.name}.json`} client={props.client ?? false} data={props.fileContent}/>
* **text** mode lets you edit the JSON directly.
* **tree** mode gives you a guided point-and-click editing experience that always keeps the JSON *syntax* valid.
or <SchemaLink lower objectName={props.config}/>
Both modes **validate** that the configuraion is correct. Any errors show up as squiggly lines.
After you finish editing, switch to **text** and then copy all text to get a completed config.
</DetailsAdmo>
</div>
<div className="fileEditorMobile">
<DetailsAdmo type="warning" summary="Config Example Mobile Experience">
This displays an **example config file** of a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> [File Configuration](/configuration?configType=aio#configuration-types) that adheres to the shown **Config Structure**.
The example config file editor is meant for a larger screen experience so **only the read-only example is shown.** Please use this site on a tablet/laptop/desktop to enable file editor features.
</DetailsAdmo>
</div>
<FileConfigExample
filePathName={`CONFIG/${props.name}.json`}
configureAs={props.client ? 'client' : 'source'}
data={props.fileContent}
schemaName={props.config}
readOnlyAtWidth={500}
/>
</div>
</div>
</FileType>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOType>
<div
className="schemaWrapper"
style={{
display: "flex"
}}
>
<div style={{ padding: '0 1em 0 1em', flexBasis: 'min-content', flexGrow: 2}}>
<DetailsAdmo type="note" summary="Config Structure">
This displays the **structure** of the [all-in-one (`config.json`) configuration](/configuration?configType=aio#configuration-types) with all possible properties, their shape, and descriptions/types. Use this to understand how to write a valid config.
<AIOExample data={props.fileContent} client={props.client ?? false} name={props.name}/>
Select the <strong children={props.name}/> type from the respective <code children={props.client ? 'clients' : 'sources'}/> node below, then expand it.
</DetailsAdmo>
<ExplorerFetch name={`schemas/aio`} />
</div>
<div style={{ flexBasis: 'auto', flexGrow: 3 }}>
<div className="fileEditor">
<DetailsAdmo type="note" summary="Config Example">
This displays an **example config file** of a [all-in-one (`config.json`) configuration](/configuration?configType=aio#configuration-types) that includes a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> that adheres to the shown **Config Structure**.
* **text** mode lets you edit the JSON directly.
* **tree** mode gives you a guided point-and-click editing experience that always keeps the JSON *syntax* valid.
or <SchemaLink lower objectName={props.config}/>
Both modes **validate** that the configuraion is correct. Any errors show up as squiggly lines.
After you finish editing, switch to **text** and then copy all text to get a completed config.
</DetailsAdmo>
</div>
<div className="fileEditorMobile">
<DetailsAdmo type="warning" summary="Config Example Mobile Experience">
This displays an **example config file** of a [all-in-one (`config.json`) configuration](/configuration?configType=aio#configuration-types) that includes a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> that adheres to the shown **Config Structure**.
The example config file editor is meant for a larger screen experience so **only the read-only example is shown.** Please use this site on a tablet/laptop/desktop to enable file editor features.
</DetailsAdmo>
</div>
<FileConfigExample
filePathName={`CONFIG/config.json`}
configureAs={props.client ? 'client' : 'source'}
data={props.fileContent}
schemaName="aio"
name={props.name}
readOnlyAtWidth={500}
aio
/>
</div>
</div>
</AIOType>
</TabItem>
</Tabs>
</Tabs>
@@ -0,0 +1,26 @@
.schemaWrapper {
margin-top: 1em;
display: flex;
flex-direction: row;
row-gap: 1em;
column-gap: 1em;
}
.fileEditorMobile {
display: none;
}
@media screen and (max-width: 1378px) {
.schemaWrapper {
flex-direction: column;
}
}
@media screen and (max-width: 500px) {
.fileEditor {
display: none;
}
.fileEditorMobile {
display: block
}
}
-44
View File
@@ -1,44 +0,0 @@
import React from "react"
import BrowserOnly from "@docusaurus/BrowserOnly"
import { useColorMode } from "@docusaurus/theme-common"
import JSONSchemaEditor from "@theme/JSONSchemaEditor"
// Context
import { usePlaygroundContext } from "@site/src/contexts/PlaygroundContext"
// Common stringify of the JSON
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t")
function JSONSchemaDataInner(): JSX.Element {
const {
state: { userSchema, editorRef, data: value },
updateState,
} = usePlaygroundContext()
const { colorMode } = useColorMode()
return (
<div style={{ boxSizing: "border-box", width: "50%" }}>
<JSONSchemaEditor
value={value}
schema={userSchema}
theme={colorMode === "dark" ? "vs-dark" : "vs"}
editorDidMount={(editor) => {
updateState({ editorRef: editor })
}}
height={"70vh"}
key={STRINGIFY_JSON(userSchema)}
/>
</div>
)
}
export default function JSONSchemaDataComponent(): JSX.Element {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
return <JSONSchemaDataInner />
}}
</BrowserOnly>
)
}
+26
View File
@@ -0,0 +1,26 @@
.schemaWrapper2 {
margin: 1em;
display: flex;
flex-direction: row;
row-gap: 1em;
column-gap: 1em;
}
.fileEditorMobile2 {
display: none;
}
@media screen and (max-width: 1280px) {
.schemaWrapper2 {
flex-direction: column;
}
}
@media screen and (max-width: 500px) {
.fileEditor2 {
display: none;
}
.fileEditorMobile2 {
display: block
}
}
+89 -35
View File
@@ -1,50 +1,104 @@
import React from "react"
import React, {useEffect, useState} from "react"
import BrowserOnly from "@docusaurus/BrowserOnly"
import {useColorMode, useWindowSize} from '@docusaurus/theme-common';
import CodeBlock from '@theme/CodeBlock';
import Schema from "@site/static/aio.json";
import Schema from "@site/static/schemas/aio.json";
import ConfigExample from "@site/static/configExample.json";
import JSONSchemaViewer from "@theme/JSONSchemaViewer"
import JSONSchemaEditor from "@theme/JSONSchemaEditor";
import {useColorMode} from "@docusaurus/theme-common";
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t")
import './Playground.scss';
import { JSONSchema, JsonSchemaViewer } from 'cf-json-schema-viz';
import "./json-schema-viewer-styles.css";
import "./modern-json-react-styles.css";
import { Mode, ReactJsonEditor, createAjvValidator, type Content } from 'modern-react-json-editor';
import f from "ajv-formats"
import DetailsAdmo from "./AdmonitionDetails";
const validator = createAjvValidator({
schema: Schema,
ajvOptions: {
strict: "log",
strictSchema: "log",
allowUnionTypes: true,
},
onCreateAjv: (ajv) => {
ajv.addKeyword('deprecationMessage');
f.default(ajv);
return ajv;
}
});
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
function PlaygroundInner(): JSX.Element {
const { colorMode } = useColorMode()
const {
colorMode, // the "effective" color mode, never null
// colorModeChoice, // the color mode chosen by the user, can be null
// setColorMode, // set the color mode chosen by the user
} = useColorMode();
const windowSize = useWindowSize({desktopBreakpoint: 500});
const [data, setData] = useState<Content>({json: ConfigExample });
return (
<div
style={{
display: "flex",
flexDirection: "column",
overflowY: "hidden",
overflowX: "hidden",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<div style={{width: '50%'}}>
<JSONSchemaViewer
schema={Schema}
showExamples={true}
<div className="schemaWrapper2">
<div style={{ padding: '0 1em 0 1em', flexBasis: 'min-content', flexGrow: 2}}>
<DetailsAdmo type="note" summary="Config Structure">
<p>This displays the <strong>structure</strong> of the <a href="/configuration?configType=aio#configuration-types">all-in-one (<code>config.json</code>) configuration</a> with all possible properties, their shape, and descriptions/types.</p>
<p>Use this to understand how to write a valid config.</p>
</DetailsAdmo>
<JsonSchemaViewer
schema={Schema as JSONSchema}
// expanded={false}
// hideTopBar={false}
renderRootTreeLines={true}
emptyText="No schema defined"
defaultExpandedDepth={0}
markup
/>
</div>
<div style={{ boxSizing: "border-box", width: "50%" }}>
<JSONSchemaEditor
value={STRINGIFY_JSON(ConfigExample)}
theme={colorMode === "dark" ? "vs-dark" : "vs"}
schema={Schema}
height={"70vh"}
/>
<div style={{ flexBasis: 'auto', flexGrow: 3 }}>
<div className="fileEditor2">
<DetailsAdmo type="note" summary="Config Example">
<p>
This displays an <strong>example config file</strong> of a <a href="/configuration?configType=aio#configuration-types">all-in-one (<code>config.json</code>) configuration</a> that adheres to the shown <strong>Config Structure.</strong>
</p>
<ul>
<li>
<strong>text</strong> mode lets you edit the JSON directly.
</li>
<li>
<strong>tree</strong> mode gives you a guided point-and-click editing experience that always keeps the JSON <i>syntax</i> valid.
</li>
</ul>
<p>Both modes <strong>validate</strong> that the configuraion is correct. Any errors show up as squiggly lines.</p>
<p>After you finish editing, switch to <strong>text</strong> and then copy all text to get a completed config.</p>
</DetailsAdmo>
</div>
<div className="fileEditorMobile2">
<DetailsAdmo type="warning" summary="Config Example Mobile Experience">
<p>
This displays an <strong>example config file</strong> of a <a href="/configuration?configType=aio#configuration-types">all-in-one (<code>config.json</code>) configuration</a> that adheres to the shown <strong>Config Structure.</strong>
</p>
<p>
The example config file editor is meant for a larger screen experience so <strong>only the read-only example is shown.</strong> Please use this site on a tablet/laptop/desktop to enable file editor features.
</p>
</DetailsAdmo>
</div>
{windowSize === 'mobile' ? (<CodeBlock title={`CONFIG/config.json`} language="json5">{JSON.stringify(data, null, 2)}</CodeBlock>) : (
<ReactJsonEditor
validator={validator}
theme={colorMode}
content={data}
mode={"text" as Mode.text}
style={{height: 'initial'}}
/>
)}
</div>
</div>
</div>
)
}
@@ -0,0 +1,38 @@
import React, {useEffect, useState} from "react"
import BrowserOnly from "@docusaurus/BrowserOnly"
//import './Explorer.scss';
import { JsonSchemaViewer } from 'cf-json-schema-viz';
import '../json-schema-viewer-styles.css'
export interface ExplorerProps {
schema: object
}
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
export default function Explorer(props: ExplorerProps): JSX.Element {
//injectStyles();
return (
<div>
<JsonSchemaViewer
schema={props.schema}
renderRootTreeLines={true}
emptyText="No schema defined"
defaultExpandedDepth={0}
markup
/>
</div>
)
}
// export default function PlaygroundComponent(): JSX.Element {
// return (
// <BrowserOnly fallback={<div>Loading...</div>}>
// {() => {
// return <PlaygroundInner />
// }}
// </BrowserOnly>
// )
// }
@@ -0,0 +1,48 @@
import React, {useEffect, useState, Fragment} from "react"
//import BrowserOnly from "@docusaurus/BrowserOnly"
import Explorer from './Explorer';
import useBaseUrl from '@docusaurus/useBaseUrl';
export interface ExplorerFetchProps {
name: string
}
export default function ExplorerFetchFunc(props: ExplorerFetchProps): JSX.Element {
const [schema, setSchema] = useState(undefined as undefined | Error | object);
const url = useBaseUrl(`/${props.name}.json`);
useEffect( () => {
fetch(
url,
{
headers: {
'Accept': 'application/json',
}
}
)
.then((response) => response.json())
.then((data) => setSchema(data))
.catch( (err) => setSchema(err) )
}, [setSchema, url])
return (
<Fragment>
{schema === undefined && <div>Loading ...</div>}
{schema !== undefined && schema instanceof Error && <div>Houston we have a problem : {schema.message}</div>}
{schema !== undefined && !(schema instanceof Error) && <Explorer schema={schema}/>}
</Fragment>
)
}
// export default function ExplorerFetch(props: ExplorerFetchProps): JSX.Element {
// return (
// <BrowserOnly fallback={<div>Loading...</div>}>
// {() => {
// return <ExplorerFetchFunc {...props} />
// }}
// </BrowserOnly>
// )
// }
@@ -0,0 +1,76 @@
import React, { useEffect, useState, useMemo } from "react"
import BrowserOnly from "@docusaurus/BrowserOnly"
import { useColorMode } from '@docusaurus/theme-common';
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t");
import "../modern-json-react-styles.css";
import { JSONSchema, Mode, ReactJsonEditor, createAjvValidator, type Content, type Validator } from 'modern-react-json-editor';
import f from "ajv-formats"
import json5 from 'json5';
export interface SchemaEditorProps {
config: object | string
schema?: object
height?: string
}
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
function SchemaEditorInner(props: SchemaEditorProps): JSX.Element {
const validator: Validator | undefined = useMemo(() => {
if(props.schema === undefined) {
return undefined;
}
return createAjvValidator({
schema: props.schema as JSONSchema,
ajvOptions: {
strict: "log",
strictSchema: "log",
allowUnionTypes: true,
},
onCreateAjv: (ajv) => {
ajv.addKeyword('deprecationMessage');
f.default(ajv);
return ajv;
}
});
}, [props.schema]);
const initialData = useMemo(() => {
let content: object;
if(typeof props.config === 'string') {
content = json5.parse(props.config);
} else {
content = props.config;
}
return {json: content};
},[props.config])
// const [data, setData] = useState({ json: props.config });
const {
colorMode, // the "effective" color mode, never null
} = useColorMode();
//const val = typeof props.config === 'object' ? STRINGIFY_JSON(props.config) : props.config;
return (
<ReactJsonEditor
validator={validator}
theme={colorMode}
mode={"text" as Mode.text}
content={initialData}
style={{height: 'initial'}}
/>
)
}
export default function SchemaEditor(props: SchemaEditorProps): JSX.Element {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
return <SchemaEditorInner {...props} />
}}
</BrowserOnly>
)
}
@@ -0,0 +1,74 @@
import React, {useEffect, useState, Fragment} from "react"
import useBaseUrl from '@docusaurus/useBaseUrl';
import SchemaEditor from "./SchemaEditor";
import json5 from 'json5';
export interface SchemaEditorFetchProps {
configContent: string | object
schemaName?: string
}
export default function ExplorerFetchFunc(props: SchemaEditorFetchProps): JSX.Element {
const {
schemaName
} = props;
const [schema, setSchema] = useState(undefined as undefined | Error | object | false);
const [config, setConfig] = useState(undefined as undefined | Error | object);
const schemaUrl = schemaName === undefined ? undefined : useBaseUrl(`schemas/${props.schemaName}.json`);
//const configUrl = props.configName !== undefined ? useBaseUrl(`/${props.configName}.json`) : undefined;
useEffect( () => {
if(schemaUrl === undefined) {
setSchema(false);
} else {
fetch(
schemaUrl,
{
headers: {
'Accept': 'application/json',
}
}
)
.then((response) => response.json())
.then((data) => setSchema(data))
.catch( (err) => setSchema(err) )
}
}, [setSchema, schemaUrl])
// useEffect( () => {
// if(configUrl !== undefined) {
// fetch(
// configUrl,
// {
// headers: {
// 'Accept': 'application/json',
// }
// }
// )
// .then((response) => response.json())
// .then((data) => setConfig(data))
// .catch( (err) => setConfig(err) )
// } else if(props.configContent !== undefined) {
// if(typeof props.configContent === 'string') {
// setConfig(json5.parse(props.configContent));
// } else {
// setConfig(props.configContent);
// }
// }
// }, [setConfig, configUrl, props.configContent])
return (
<Fragment>
{schema === undefined && <div>Loading ...</div>}
{schema !== undefined && schema instanceof Error && <div>Houston we have a problem : {schema.message}</div>}
{schema !== undefined && !(schema instanceof Error) &&
<SchemaEditor
schema={schema === false ? undefined : schema}
config={props.configContent}
/>}
</Fragment>
)
}
@@ -0,0 +1,480 @@
/* JSON Schema Viewer - Pure CSS with CSS Custom Properties */
/* ========================================
Theme Variables - Light Mode (default)
Define at :root so portaled elements inherit them
======================================== */
:root {
--jsv-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--jsv-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
--jsv-font-size: 0.875rem;
/* Colors - Light Mode defaults */
--jsv-color-body: #1a1a1a;
--jsv-color-muted: #6b7280;
--jsv-color-border: #e5e7eb;
--jsv-color-canvas: #f4f4f5;
--jsv-color-canvas-tint: #f4f4f5;
--jsv-color-bg: #ffffff;
/* Semantic colors - Cloudflare orange as primary */
--jsv-color-primary: #f6821f;
--jsv-color-danger: #dc2626;
--jsv-color-warning: #f97316;
--jsv-color-success: #16a34a;
}
/* ========================================
Theme Variables - Dark Mode
Cloudflare docs dark theme colors
======================================== */
:root[data-theme="dark"],
[data-theme="dark"] {
--jsv-color-body: #e4e4e7;
--jsv-color-muted: #a1a1aa;
--jsv-color-border: #3f3f46;
--jsv-color-canvas: #27272a;
--jsv-color-canvas-tint: #3f3f46;
--jsv-color-bg: #18181b;
--jsv-color-primary: #f6821f;
--jsv-color-danger: #f87171;
--jsv-color-warning: #fb923c;
--jsv-color-success: #4ade80;
}
/* ========================================
Base Styles
======================================== */
.jsv-root {
font-size: var(--jsv-font-size);
font-family: var(--jsv-font-sans);
color: var(--jsv-color-body);
line-height: 1.5;
position: relative;
}
.jsv-content {
padding-left: 2rem;
padding-right: 1rem;
padding-bottom: 0.5rem;
padding-top: 0.375rem;
}
/* ========================================
Schema Row
======================================== */
.jsv-row {
position: relative;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
display: flex;
align-items: flex-start;
max-width: 100%;
}
.jsv-row-content {
display: flex;
flex-direction: column;
gap: 0.125rem;
max-width: 100%;
flex: 1;
min-width: 0;
}
.jsv-row-header {
display: flex;
align-items: center;
max-width: 100%;
gap: 0.25rem;
}
/* Nubbin - horizontal connector from left border to property */
.jsv-nubbin {
width: 0.75rem;
height: 0;
border-top: 1px solid var(--jsv-color-border);
flex-shrink: 0;
margin-top: 0.5rem;
margin-right: 0.5rem;
margin-left: -1rem;
}
.jsv-nubbin-short {
width: 0.25rem;
}
/* ========================================
Child Stack (nested content with left border)
======================================== */
.jsv-child-stack {
font-size: var(--jsv-font-size);
}
.jsv-child-stack-nested {
border-left: 1px solid var(--jsv-color-border);
margin-left: 0.5rem;
padding-left: 1rem;
margin-top: 0.25rem;
}
/* ========================================
Property Name
======================================== */
.jsv-property-name {
font-family: var(--jsv-font-mono);
font-weight: 600;
margin-right: 0.5rem;
flex-shrink: 0;
}
/* ========================================
Type Display
======================================== */
.jsv-type {
color: var(--jsv-color-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.jsv-types-container {
display: flex;
align-items: baseline;
font-size: var(--jsv-font-size);
min-width: 0;
}
/* ========================================
Badges (required, deprecated, read-only)
======================================== */
.jsv-badges {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.jsv-badge {
font-size: 0.75rem;
}
.jsv-badge-required {
color: var(--jsv-color-warning);
}
.jsv-badge-deprecated {
color: var(--jsv-color-warning);
}
.jsv-badge-readonly {
color: var(--jsv-color-muted);
}
/* ========================================
Divider (horizontal line on hover)
======================================== */
.jsv-divider {
height: 1px;
flex: 1;
margin-left: 0.75rem;
margin-right: 0.75rem;
min-width: 1rem;
background-color: transparent;
transition: background-color 0.15s ease;
}
.jsv-divider-visible {
background-color: var(--jsv-color-border);
}
/* ========================================
Description
======================================== */
.jsv-description {
font-size: var(--jsv-font-size);
color: var(--jsv-color-muted);
margin-top: 0.0625rem;
line-height: 1.4;
}
/* ========================================
Validations
======================================== */
.jsv-validations {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: center;
margin-top: 0.125rem;
}
.jsv-validations-row {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: center;
}
.jsv-validation {
display: flex;
align-items: baseline;
gap: 0.375rem;
color: var(--jsv-color-muted);
font-size: 0.75rem;
}
.jsv-validation-label {
flex-shrink: 0;
}
.jsv-validation-values {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
flex: 1;
}
.jsv-validation-value {
padding: 0.0625rem 0.375rem;
background-color: var(--jsv-color-canvas-tint);
color: var(--jsv-color-muted);
border: 1px solid var(--jsv-color-border);
border-radius: 0.25rem;
font-family: var(--jsv-font-mono);
font-size: 0.75rem;
word-break: break-all;
}
/* ========================================
Caret (expand/collapse icon)
======================================== */
.jsv-caret {
width: 1.25rem;
height: 1.25rem;
margin-left: -1.5rem;
margin-right: 0.25rem;
color: var(--jsv-color-muted);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
flex-shrink: 0;
transition: transform 0.15s ease;
}
.jsv-caret:hover {
color: var(--jsv-color-body);
}
.jsv-caret-expanded {
transform: rotate(90deg);
}
/* ========================================
Error Display
======================================== */
.jsv-error {
color: var(--jsv-color-danger);
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
}
/* ========================================
Change Annotations (added/removed/modified)
======================================== */
.jsv-annotation {
position: absolute;
width: 0.25rem;
height: 1rem;
border-radius: 0.125rem;
top: 50%;
transform: translateY(-50%);
}
.jsv-annotation-added {
background-color: var(--jsv-color-success);
}
.jsv-annotation-removed {
background-color: var(--jsv-color-danger);
}
.jsv-annotation-modified {
background-color: var(--jsv-color-warning);
}
/* ========================================
Path Crumbs (sticky breadcrumb header)
Wrapper is sticky with height:0 (no layout impact)
Inner crumbs div is absolute, overlays content
======================================== */
.jsv-crumbs {
height: 2rem;
padding-left: 2rem;
padding-right: 1rem;
font-family: var(--jsv-font-mono);
font-size: var(--jsv-font-size);
line-height: 1;
z-index: 20;
background-color: var(--sl-color-bg, var(--jsv-color-bg, #fff));
color: var(--jsv-color-muted);
display: flex;
align-items: center;
gap: 0.25rem;
border-bottom: 1px solid var(--jsv-color-border);
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
box-sizing: border-box;
/* Hidden by default */
opacity: 0;
pointer-events: none;
}
[data-theme="dark"] .jsv-crumbs,
:root[data-theme="dark"] .jsv-crumbs {
border-bottom-color: var(--sl-color-gray-5, #52525b);
}
.jsv-crumbs[data-visible="true"] {
opacity: 1;
pointer-events: auto;
}
.jsv-crumb-current {
color: var(--jsv-color-body);
font-weight: 600;
}
/* ========================================
Select / Menu Dropdown
======================================== */
.jsv-select-trigger {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.125rem 0.5rem;
font-size: var(--jsv-font-size);
font-family: var(--jsv-font-sans);
background-color: transparent;
border: 1px solid var(--jsv-color-border);
border-radius: 0.25rem;
cursor: pointer;
color: var(--jsv-color-muted);
line-height: 1.4;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.jsv-select-trigger:hover {
background-color: var(--jsv-color-canvas);
border-color: var(--jsv-color-muted);
}
.jsv-select-trigger:focus {
outline: none;
box-shadow: 0 0 0 2px var(--jsv-color-primary);
}
.jsv-select-positioner {
z-index: 9999 !important;
position: relative;
}
.jsv-select-popup {
background-color: var(--jsv-color-bg);
border: 1px solid var(--jsv-color-border);
border-radius: 0.375rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 0.25rem 0;
min-width: 10rem;
overflow: hidden;
font-family: var(--jsv-font-sans);
font-size: var(--jsv-font-size);
}
.jsv-select-item {
padding: 0.5rem 0.75rem;
padding-left: 2rem;
font-size: var(--jsv-font-size);
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
outline: none;
color: var(--jsv-color-body);
background-color: var(--jsv-color-bg);
position: relative;
transition: background-color 0.1s ease;
}
.jsv-select-item:hover,
.jsv-select-item:focus,
.jsv-select-item[data-highlighted] {
background-color: var(--jsv-color-canvas);
}
.jsv-select-item[data-selected] {
font-weight: 600;
}
.jsv-select-item-indicator {
position: absolute;
left: 0.625rem;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
color: var(--jsv-color-primary);
}
/* ========================================
Top Level Styles
======================================== */
.jsv-top-label {
font-family: var(--jsv-font-mono);
font-weight: 600;
font-size: var(--jsv-font-size);
padding-bottom: 0.5rem;
}
.jsv-top-section {
display: flex;
align-items: center;
gap: 0.75rem;
padding-bottom: 0.5rem;
}
.jsv-combiner-label {
display: flex;
align-items: center;
color: var(--jsv-color-muted);
font-size: var(--jsv-font-size);
}
/* ========================================
Link Styles
======================================== */
.jsv-link {
color: var(--jsv-color-primary);
cursor: pointer;
text-decoration: none;
margin-left: 0.5rem;
}
.jsv-link:hover {
text-decoration: underline;
}
/* ========================================
Misc Helpers
======================================== */
.jsv-cursor-pointer {
cursor: pointer;
}
.jsv-ml-2 {
margin-left: 0.5rem;
}
@@ -0,0 +1 @@
.jse-theme-dark{--jse-theme: dark;--jse-theme-color: #2f6dd0;--jse-theme-color-highlight: #467cd2;--jse-background-color: #1e1e1e;--jse-text-color: #d4d4d4;--jse-text-color-inverse: #4d4d4d;--jse-main-border: 1px solid #4f4f4f;--jse-menu-color: #fff;--jse-modal-background: #2f2f2f;--jse-modal-overlay-background: rgba(0, 0, 0, .5);--jse-modal-code-background: #2f2f2f;--jse-tooltip-color: var(--jse-text-color);--jse-tooltip-background: #4b4b4b;--jse-tooltip-border: 1px solid #737373;--jse-tooltip-action-button-color: inherit;--jse-tooltip-action-button-background: #737373;--jse-panel-background: #333333;--jse-panel-background-border: 1px solid #464646;--jse-panel-color: var(--jse-text-color);--jse-panel-color-readonly: #737373;--jse-panel-border: 1px solid #3c3c3c;--jse-panel-button-color-highlight: #e5e5e5;--jse-panel-button-background-highlight: #464646;--jse-navigation-bar-background: #656565;--jse-navigation-bar-background-highlight: #7e7e7e;--jse-navigation-bar-dropdown-color: var(--jse-text-color);--jse-context-menu-background: #4b4b4b;--jse-context-menu-background-highlight: #595959;--jse-context-menu-separator-color: #595959;--jse-context-menu-color: var(--jse-text-color);--jse-context-menu-pointer-background: #737373;--jse-context-menu-pointer-background-highlight: #818181;--jse-context-menu-pointer-color: var(--jse-context-menu-color);--jse-key-color: #9cdcfe;--jse-value-color: var(--jse-text-color);--jse-value-color-number: #b5cea8;--jse-value-color-boolean: #569cd6;--jse-value-color-null: #569cd6;--jse-value-color-string: #ce9178;--jse-value-color-url: #ce9178;--jse-delimiter-color: #949494;--jse-edit-outline: 2px solid var(--jse-text-color);--jse-selection-background-color: #464646;--jse-selection-background-inactive-color: #333333;--jse-hover-background-color: #343434;--jse-active-line-background-color: rgba(255, 255, 255, .06);--jse-search-match-background-color: #343434;--jse-collapsed-items-background-color: #333333;--jse-collapsed-items-selected-background-color: #565656;--jse-collapsed-items-link-color: #b2b2b2;--jse-collapsed-items-link-color-highlight: #ec8477;--jse-search-match-color: #724c27;--jse-search-match-outline: 1px solid #966535;--jse-search-match-active-color: #9f6c39;--jse-search-match-active-outline: 1px solid #bb7f43;--jse-tag-background: #444444;--jse-tag-color: #bdbdbd;--jse-table-header-background: #333333;--jse-table-header-background-highlight: #424242;--jse-table-row-odd-background: rgba(255, 255, 255, .1);--jse-input-background: #3d3d3d;--jse-input-border: var(--jse-main-border);--jse-button-background: #808080;--jse-button-background-highlight: #7a7a7a;--jse-button-color: #e0e0e0;--jse-button-secondary-background: #494949;--jse-button-secondary-background-highlight: #5d5d5d;--jse-button-secondary-background-disabled: #9d9d9d;--jse-button-secondary-color: var(--jse-text-color);--jse-a-color: #55abff;--jse-a-color-highlight: #4387c9;--jse-svelte-select-background: #3d3d3d;--jse-svelte-select-border: 1px solid #4f4f4f;--list-background: #3d3d3d;--item-hover-bg: #505050;--multi-item-bg: #5b5b5b;--input-color: #d4d4d4;--multi-clear-bg: #8a8a8a;--multi-item-clear-icon-color: #d4d4d4;--multi-item-outline: 1px solid #696969;--list-shadow: 0 2px 8px 0 rgba(0, 0, 0, .4);--jse-color-picker-background: #656565;--jse-color-picker-border-box-shadow: #8c8c8c 0 0 0 1px}
@@ -1,2 +1 @@
[**AIO** Config Type](/configuration?configType=aio#configuration-types)
{props.children}
@@ -1,3 +1,5 @@
[**ENV** Config Type](/configuration?configType=env#configuration-types)
:::note[Configuration Type]
This is configuration for the **ENV** [Config Type](/configuration?configType=env#configuration-types).
:::
{props.children}
@@ -1,3 +1 @@
[**File** Config Type](/configuration?configType=file#configuration-types)
{props.children}
+116
View File
@@ -0,0 +1,116 @@
/**
* Inspired by typings from https://github.com/michal-worwag/hooks-ts/blob/main/packages/hooks-ts/src/useLocalStorage/useLocalStorage.ts
* and global updates/dedup from https://github.com/kaansey/react-localstorage-hook/blob/master/src/useLocalStorage/useLocalStorage.ts
*/
import { useState, useEffect, useCallback } from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
const localTypedStorageHook = <T>(key: string, initialValue: T, json: boolean = true): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
const item = window.localStorage.getItem(key);
if (json) {
try {
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
} else {
try {
if (null === item) {
return initialValue;
}
if (item === 'true') {
return true as T;
}
if (item === 'false') {
return false as T;
}
if (isNumeric(item)) {
if (item.includes('.')) {
return parseFloat(item) as T;
}
return parseInt(item) as T;
}
return item as T;
} catch (error) {
console.error(error);
return initialValue;
}
}
});
const updateValue = useCallback(
newValue => {
setStoredValue(() => {
if(json) {
localStorage.setItem(key, JSON.stringify(newValue))
} else {
localStorage.setItem(key, newValue)
}
return newValue
})
},
[key]
);
useEffect(() => {
const onStorage = (event: any) => {
if (event.storageArea === localStorage && event.key === key) {
setStoredValue(
event.newValue === null ? initialValue : event.newValue
)
}
}
window.addEventListener('storage', onStorage)
return () => window.removeEventListener('storage', onStorage)
});
return [storedValue, updateValue];
}
const isNumeric = (num: any) => (typeof(num) === 'number' || typeof(num) === "string" && num.trim() !== '') && !isNaN(num as number);
const createTypedLocalStorageHook = <T>(key: string, defaultValue: T, json?: boolean) => {
const updates: Array<any> = []
return () => {
const [value, setValue] = localTypedStorageHook<T>(key, defaultValue, json)
const updateValue = useCallback((newValue: T) => {
for (const update of updates) {
update(newValue)
}
}, [])
useEffect(() => {
updates.push(setValue)
return () => {
updates.splice(updates.indexOf(setValue), 1)
}
}, [setValue])
return [value, updateValue]
}
}
const hookInstances: Record<string, any> = {}
export const useTypedLocalStorage = <T>(key: string, defaultValue: T, json?: boolean): [T, (value: T) => void] => {
if (key in hookInstances) {
return hookInstances[key]()
}
// prevents usage during server static rendering
if(!ExecutionEnvironment.canUseDOM) {
return [defaultValue, () => null];
}
hookInstances[key] = createTypedLocalStorageHook(key, defaultValue, json)
return hookInstances[key]()
}
@@ -1,50 +0,0 @@
import { useContext, createContext } from "react"
// Type I need for useRef
import type { MonacoEditorTypes } from "@theme/MonacoEditor"
export type State = {
// The full schema
// We might need to scope it with a JSON Pointer
fullSchema: unknown
// The current schema displayed (after the json pointer)
userSchema: unknown
// The current json pointer
jsonPointer: string
// Schema editor ref
schemaRef: undefined | MonacoEditorTypes.IStandaloneCodeEditor
// Data editor ref
editorRef: undefined | MonacoEditorTypes.IStandaloneCodeEditor
// Data on the data editor
// When using "Infer schema from data", it would be annoying to lose our written
data: string
// Errors in schema
schemaErrors: MonacoEditorTypes.IMarkerData[]
// Errors in data
dataErrors: MonacoEditorTypes.IMarkerData[]
}
export type Playground = {
// state
state: State
// update function
updateState: (_: Partial<State>) => void
}
export const PlaygroundContext = createContext<Playground>({
state: {
fullSchema: {},
userSchema: {},
jsonPointer: "",
data: "{}",
schemaRef: undefined,
editorRef: undefined,
schemaErrors: [],
dataErrors: [],
},
updateState: () => {},
})
export const usePlaygroundContext = () => useContext(PlaygroundContext)
export const PlaygroundContextProvider = PlaygroundContext.Provider
+1 -1
View File
@@ -32,7 +32,7 @@ export default function Playground(): JSX.Element {
return (
<Layout
title={`Playground`}
description="Playground of docusaurus-json-schema-plugin"
description="Playground"
>
<PlaygroundComponent />
</Layout>
Binary file not shown.
+92
View File
@@ -0,0 +1,92 @@
import type { DocsPageData, ImageRenderer } from '@bony_chops/docusaurus-og'
import { readFileSync } from 'fs'
import { join } from 'path'
// const logo = (<svg
// id="Layer_1"
// data-name="Layer 1"
// xmlns="http://www.w3.org/2000/svg"
// width="128"
// height="128"
// viewBox="0 0 128 128"
// fill="#c34a36"
// >
// <path className="cls-1" d="M76.94,65.49c.87-7.85-5.27-14.51-12.94-14.51-4.56,0-8.57,2.36-10.9,5.91h-4.55c2.36-5.11,7.17-8.87,12.95-9.71V20c0-11.05-8.95-20-20-20H5C2.24,0,0,2.24,0,5V123c0,2.76,2.24,5,5,5H41.5c11.05,0,20-8.95,20-20v-27.17c-5.78-.84-10.59-4.6-12.95-9.72h4.55c2.59,3.96,7.27,6.43,12.46,5.82,6.08-.71,10.71-5.37,11.39-11.45ZM15.3,94.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Zm11.12,14.55c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm37.58-39.53c-2.28,0-4.23-1.41-5.02-3.41h-21.43v28.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v28.39h21.43c.93-2.33,3.42-3.86,6.18-3.29,2.05,.42,3.7,2.08,4.13,4.12,.73,3.5-1.92,6.58-5.29,6.58Z" />
// <path className="cls-1" d="M123,0h-36.5c-11.05,0-20,8.95-20,20v27.18c7.55,1.12,13.5,7.2,14.4,14.82h9.55v-28.28c0-1,.68-1.92,1.66-2.08,1.25-.2,2.34,.76,2.34,1.97v60.67c0,1-.68,1.92-1.66,2.08-1.25,.2-2.34-.76-2.34-1.97v-28.39h-9.55c-.9,7.62-6.84,13.7-14.4,14.82v27.18c0,11.05,8.95,20,20,20h36.5c2.76,0,5-2.24,5-5V5c0-2.76-2.24-5-5-5Zm-17.42,108.94c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm11.12-14.55c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Z" />
// </svg>);
const DocContent = (props: { data: DocsPageData }) => {
const { data } = props;
return (
<div style={{
height: '100%',
width: '100%',
display: 'flex',
textAlign: 'center',
alignItems: 'flex-start',
backgroundColor: "#1b1b1d",
color: 'white',
justifyContent: 'flex-start',
flexDirection: 'column',
flexWrap: 'wrap',
padding: '3rem 3rem 0 3rem',
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '30px'
}}>
<img src="https://docs.multi-scrobbler.app/img/icon.png" width={120} height={120} />
<div style={{
fontSize: 125,
//fontWeight: 600
}}>Multi-Scrobbler</div>
</div>
<div style={{
fontSize: 50,
color: "#c8c8c8",
marginTop: '1rem'
}}
>Listen everywhere, Scrobble anywhere</div>
<div style={{
marginTop: '4rem',
display: 'flex',
alignContent: 'flex-start',
justifyContent: 'flex-start',
flexDirection: 'column',
gap: '10px',
paddingLeft: '0.5em',
fontSize: 75
}}>
<div style={{
display: 'flex',
textWrap: 'balance',
textAlign: 'left'
}}>{data.metadata.frontMatter?.description ?? data.metadata.title}</div>
</div>
</div>
);
}
export const docs: ImageRenderer<DocsPageData> = (data, context) => [
<DocContent data={data} />,
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: readFileSync(
// must be a STATIC (not variable) font due to a bug in satori
// https://github.com/vercel/satori/issues/162
// use static-font.sh to convert a variable font to static
join(__dirname, '../../src/socialCard/AdwaitaSans-Regular-StaticSubset.ttf'),
),
weight: 400,
style: 'normal',
},
],
},
]
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# based on https://github.com/Kikobeats/og.kikobeats.com/blob/master/static-font.sh
# 1) Freeze SF Pro to Bold (700) and make it fully static
fonttools varLib.instancer AdwaitaSans-Regular.ttf \
wght=400 \
--static \
-o AdwaitaSans-Regular-Static.ttf
# 2) Subset to only the glyphs you actually need (Satori-friendly)
fonttools subset AdwaitaSans-Regular-Static.ttf \
--text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#\$%^&*()-_=+[]{};:'\",.<>/? " \
--no-hinting \
--name-legacy \
--output-file=AdwaitaSans-Regular-StaticSubset.ttf
+5 -1
View File
@@ -9,6 +9,8 @@ import EnvType from "@site/src/components/snippets/_env-config.mdx";
import FileType from "@site/src/components/snippets/_file-config.mdx";
import AIOType from "@site/src/components/snippets/_aio-config.mdx";
import Config from "@site/src/components/GenericConfiguration.mdx";
import Button from '../components/Button';
import ButtonGroup from '../components/ButtonGroup';
export default {
// Re-use the default mapping
@@ -20,5 +22,7 @@ export default {
FileType,
AIOType,
Config,
DetailsAdmo
DetailsAdmo,
Button,
ButtonGroup
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

@@ -0,0 +1,925 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "red", "value": 1 }
]
}
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 },
"id": 1,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "sum(multiscrobbler_source_issues)",
"refId": "A"
}
],
"title": "Total Source Issues",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "red", "value": 1 }
]
}
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 },
"id": 2,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "sum(multiscrobbler_client_issues)",
"refId": "A"
}
],
"title": "Total Client Issues",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 300000000 },
{ "color": "red", "value": 500000000 }
]
},
"unit": "decbytes"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 },
"id": 25,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "process_resident_memory_bytes{job=\"multi-scrobbler\"}",
"refId": "A"
}
],
"title": "Memory (RSS)",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"max": 1,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 0.5 },
{ "color": "red", "value": 0.8 }
]
},
"unit": "percentunit"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 },
"id": 26,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "rate(process_cpu_seconds_total{job=\"multi-scrobbler\"}[1m])",
"refId": "A"
}
],
"title": "CPU Usage",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 0.05 },
{ "color": "red", "value": 0.1 }
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 0 },
"id": 27,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_eventloop_lag_seconds{job=\"multi-scrobbler\"}",
"refId": "A"
}
],
"title": "Event Loop Lag",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 0 },
"id": 3,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": false },
"tooltip": { "mode": "single", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "sum(rate(multiscrobbler_source_discovered[5m]))",
"legendFormat": "Discoveries/sec",
"refId": "A"
}
],
"title": "Discovery Rate",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 4 },
"id": 5,
"panels": [],
"title": "Sources",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 },
"id": 6,
"options": {
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "multiscrobbler_source_discovered",
"legendFormat": "{{name}} ({{type}})",
"refId": "A"
}
],
"title": "Plays Discovered by Source",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }]
}
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 },
"id": 7,
"options": {
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "multiscrobbler_source_issues",
"legendFormat": "{{name}} ({{type}})",
"refId": "A"
}
],
"title": "Source Issues",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 },
"id": 8,
"panels": [],
"title": "Clients",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 14 },
"id": 9,
"options": {
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "multiscrobbler_client_scrobbled",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"title": "Total Scrobbles by Client",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "normal" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 14 },
"id": 10,
"options": {
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "multiscrobbler_client_queued",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"title": "Queued Plays by Client",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }]
}
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 14 },
"id": 11,
"options": {
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "multiscrobbler_client_issues",
"legendFormat": "{{name}}",
"refId": "A"
}
],
"title": "Client Issues",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 },
"id": 12,
"panels": [],
"title": "Node.js Performance",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "decbytes"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 23 },
"id": 17,
"options": {
"legend": { "calcs": ["last", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "process_resident_memory_bytes{job=\"multi-scrobbler\"}",
"legendFormat": "Resident Memory (RSS)",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_heap_size_total_bytes{job=\"multi-scrobbler\"}",
"legendFormat": "Heap Total",
"refId": "B"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_heap_size_used_bytes{job=\"multi-scrobbler\"}",
"legendFormat": "Heap Used",
"refId": "C"
}
],
"title": "Memory Usage",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 0.05 },
{ "color": "red", "value": 0.1 }
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 23 },
"id": 18,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_eventloop_lag_seconds{job=\"multi-scrobbler\"}",
"legendFormat": "Current Lag",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_eventloop_lag_mean_seconds{job=\"multi-scrobbler\"}",
"legendFormat": "Mean Lag",
"refId": "B"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_eventloop_lag_p99_seconds{job=\"multi-scrobbler\"}",
"legendFormat": "P99 Lag",
"refId": "C"
}
],
"title": "Event Loop Lag",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "percentunit"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 23 },
"id": 19,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "rate(process_cpu_user_seconds_total{job=\"multi-scrobbler\"}[5m])",
"legendFormat": "User CPU",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "rate(process_cpu_system_seconds_total{job=\"multi-scrobbler\"}[5m])",
"legendFormat": "System CPU",
"refId": "B"
}
],
"title": "CPU Usage",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 31 },
"id": 20,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_active_handles_total{job=\"multi-scrobbler\"}",
"refId": "A"
}
],
"title": "Active Handles",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 31 },
"id": 21,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "process_open_fds{job=\"multi-scrobbler\"}",
"refId": "A"
}
],
"title": "Open File Descriptors",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 8, "x": 8, "y": 31 },
"id": 22,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "rate(nodejs_gc_duration_seconds_sum{job=\"multi-scrobbler\"}[5m])",
"legendFormat": "{{kind}} GC",
"refId": "A"
}
],
"title": "Garbage Collection Time",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "normal" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "decbytes"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 8, "x": 16, "y": 31 },
"id": 23,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "nodejs_heap_space_size_used_bytes{job=\"multi-scrobbler\"}",
"legendFormat": "{{space}}",
"refId": "A"
}
],
"title": "Heap Space Breakdown",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 35 },
"id": 24,
"panels": [],
"title": "Cache Performance",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "percent"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 36 },
"id": 13,
"options": {
"legend": { "calcs": ["mean", "last"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "100 * multiscrobbler_cache_hits / (multiscrobbler_cache_hits + multiscrobbler_cache_misses)",
"legendFormat": "{{cacheType}} ({{tier}})",
"refId": "A"
}
],
"title": "Cache Hit Rate",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "ops"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 36 },
"id": 14,
"options": {
"legend": { "calcs": ["mean", "last"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "rate(multiscrobbler_cache_sets[5m])",
"legendFormat": "{{cacheType}} ({{tier}})",
"refId": "A"
}
],
"title": "Cache Set Rate",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
"fillOpacity": 10, "gradientMode": "none",
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
},
"overrides": [
{
"matcher": { "id": "byRegexp", "options": ".*hits.*" },
"properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }]
},
{
"matcher": { "id": "byRegexp", "options": ".*misses.*" },
"properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }]
}
]
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 44 },
"id": 16,
"options": {
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "multiscrobbler_cache_hits",
"legendFormat": "{{cacheType}} hits ({{tier}})",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"expr": "multiscrobbler_cache_misses",
"legendFormat": "{{cacheType}} misses ({{tier}})",
"refId": "B"
}
],
"title": "Cache Hits vs Misses",
"type": "timeseries"
}
],
"refresh": "10s",
"schemaVersion": 38,
"style": "dark",
"tags": ["multiscrobbler", "scrobbling", "music"],
"templating": {
"list": [
{
"current": { "selected": false, "text": "Prometheus", "value": "Prometheus" },
"hide": 0,
"includeAll": false,
"label": "Datasource",
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": { "from": "now-6h", "to": "now" },
"timepicker": {},
"timezone": "",
"title": "Multiscrobbler Dashboard",
"uid": "multiscrobbler",
"version": 1,
"weekStart": ""
}
+1 -1
View File
@@ -1,4 +1,4 @@
Sitemap: https://foxxmd.github.io/multi-scrobbler/sitemap.xml
Sitemap: https://docs.multi-scrobbler.app/sitemap.xml
User-agent: *
Allow: /
View File

Some files were not shown because too many files have changed in this diff Show More