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
FoxxMD 25f4095276 chore(release): Bump version 2026-02-26 20:37:22 +00:00
FoxxMD d242c133dd docs(faq): Add connection/dns guidance 2026-02-26 19:39:51 +00:00
FoxxMD da3e8044b7 docs: Add status page to monitoring section 2026-02-26 19:23:58 +00:00
FoxxMD b6d5939fbf chore: Add kasm service as extenable compose file for dev 2026-02-26 19:12:21 +00:00
FoxxMD d23ef88647 docs(discord): Clearer wording on user token usage 2026-02-26 17:21:48 +00:00
FoxxMD f374f130de fix(discord): Kasm improvements
* Fix permissions in compose example
* Better wording in advantages/disadvantages
2026-02-26 17:21:30 +00:00
FoxxMD b0c2c8611b docs(discord): Use named volume for kasmcord 2026-02-26 02:16:43 +00:00
FoxxMD 6f6f56ce64 docs(discord): Reorganize discord app container instructions and improve art examples 2026-02-26 02:06:21 +00:00
FoxxMD 4b28ec4b3a fix(discord): wrong protocol comparison 2026-02-25 21:14:12 +00:00
FoxxMD d189084c15 feat(discord): Implement retry for cover art api 2026-02-25 20:56:19 +00:00
FoxxMD 975d9540bc docs(discord): Update kasmcord with persistent config volume 2026-02-25 20:18:41 +00:00
FoxxMD 212bfba950 docs(discord): Add discord container instructions 2026-02-25 19:19:31 +00:00
FoxxMD 619e32cd83 feat(discord): Add more logging for cover art acquistion 2026-02-25 19:17:09 +00:00
FoxxMD ce9ff53ae1 feat(discord): Add debug log when no cover art found 2026-02-25 19:07:39 +00:00
FoxxMD d13fb33884 docs(discord): Improved discord app docs 2026-02-25 16:31:55 +00:00
FoxxMD 253e91250f fix(discord): Patch IPC client to catch socket error 2026-02-25 02:54:18 +00:00
FoxxMD b23668372b feat(discord): Correct position timestamp based on player last update time #468 2026-02-24 20:07:31 +00:00
FoxxMD 5b847c136e fix(discord): Don't queue scrobbles 2026-02-24 19:03:25 +00:00
FoxxMD 0d304a793d fix(scrobblers): Don't queue now playing data if scrobbler is not ready 2026-02-24 18:46:02 +00:00
FoxxMD 906e1a1a19 fix(discord): Stop scrobbler and set as auth failure on invalid client from IPC 2026-02-24 18:45:43 +00:00
FoxxMD 87f46f1e02 Improve failure mode handling for IPC discord
* Set auth failure on 4000 from server (invalid client id)
* Better logging for connection issues
* Don't fail on connect so we can keep retrying on MS activity
2026-02-24 18:31:46 +00:00
FoxxMD 56b4be1fe1 QoL features for discord-rpc library
* Add keepAlive to so close events actually happen
* DEBUG log human readable payload rather than hex
2026-02-24 18:28:37 +00:00
FoxxMD a08dcf05f1 refactor(discord): Remove activity filtering
https://github.com/FoxxMD/multi-scrobbler/issues/468#issuecomment-3952116123
2026-02-24 15:13:28 +00:00
FoxxMD 460882f6c1 fix(discord): wrong eqaulity for token check 2026-02-24 13:31:41 +00:00
FoxxMD 71fc94ba63 Missing category 2026-02-23 22:32:24 +00:00
FoxxMD b587d834bc docs: Add discord local/headless docs 2026-02-23 22:31:33 +00:00
FoxxMD 9b63e022ed Don't require media proxy for IPC 2026-02-23 21:15:37 +00:00
FoxxMD 324cbd76ee refator(discord): Move client init to build stage with more logging 2026-02-23 21:03:58 +00:00
FoxxMD ceab13f672 refactor(discord): Consolidate shared functionality 2026-02-23 20:47:22 +00:00
FoxxMD 10e17ed239 Initial IPC implementation 2026-02-23 20:15:21 +00:00
FoxxMD 4f84479513 fix: Fix labels for MS heartbeat task 2026-02-23 16:42:18 +00:00
FoxxMD 216ad4ed32 fix(discord): Stabalize heartbeat interval and logging
* Refactor sequence and gateway receive logging as labels
* Use timeout instead of interval for heartbeat to try to fix weird speedup
2026-02-23 16:42:18 +00:00
FoxxMD 85e4555322 fix(discord): Even more reconnect handling improvements
* Fix reconnect logic and ready state clearing
* Clearer and less noisy logging
* Delay clearing activities for convience on quick reconnect
2026-02-20 16:59:03 +00:00
FoxxMD a90e443a88 faster heartbeat for safety 2026-02-19 17:25:24 +00:00
FoxxMD a9c54b5f91 refactor(discord): More connection logic improvements
* Handle connect and auth separately so it can be restarted by scrobble heartbeat
* make ms-initiated reconnect use manual close/open since isosockets won't
* better error handling for auth/connection init stages
* send heartbeats even if not authed
2026-02-19 17:23:38 +00:00
FoxxMD 6d21435aab fix(discord): Use iso-websockets reconnect and workaround initial connection issues 2026-02-19 16:32:59 +00:00
FoxxMD 9c3bb11a66 more discord ws logging 2026-02-19 14:28:16 +00:00
FoxxMD d983080584 fix(discord): Fix config logging for artwork 2026-02-18 19:40:46 +00:00
FoxxMD 852b96009c docs cleanup 2026-02-18 18:13:54 +00:00
FoxxMD c82617d857 docs(discord): Update artwork guidance for cover art archive 2026-02-18 17:19:08 +00:00
FoxxMD 6f19256187 feat(discord): Use release mbid to get coverart if fallback would otherwise be used 2026-02-18 16:16:46 +00:00
FoxxMD 479ede334b fix(transform): Use temporally-invariant hash comparison
Fixes not using transform hashes due to user listening data on discover or for playing now data
2026-02-18 16:16:05 +00:00
FoxxMD e1349255de feat(scrobblers): Run preCompare transform hook on playing now data 2026-02-18 16:15:10 +00:00
FoxxMD 4a600dc01d Make art its own type 2026-02-18 15:27:09 +00:00
FoxxMD 315256ce96 feat(musicbrainz): Implement CoverArtArchive API 2026-02-18 15:25:14 +00:00
Matt Foxx bbead20723 Merge pull request #472 from FoxxMD/discordPresence
feat: Discord rich presence
2026-02-17 16:35:30 -05:00
FoxxMD bb77c1c0bb feat(discord): Use artwork url if its from a known domain 2026-02-17 21:28:53 +00:00
FoxxMD 2a2fb9ad55 refactor(discord): Update links and art
* Use pocike icon for placeholder art
* Use MS logo for small art with link to docs
* Use web link as track url, if available
* Use MB link as track or artist url
2026-02-17 21:05:26 +00:00
FoxxMD ea5b66cdae fix(discord): Fix ajv not liking defaultUrl string | false 2026-02-17 20:16:50 +00:00
FoxxMD 1a978c7ee3 fix: Fix config validation error source 2026-02-17 20:13:11 +00:00
FoxxMD cfd458f463 docs(discord): Improve artwork url guidance and application id 2026-02-17 20:12:49 +00:00
FoxxMD 61b8277822 feat(discord): Add actionable links for public URLs and musicbrainz
Add links to album art and/or small image if application id enabled
2026-02-17 19:05:17 +00:00
FoxxMD 53c8036e34 feat(discord): Add more presence configuration options 2026-02-16 21:08:23 +00:00
FoxxMD f5c8ce5109 more now playing fixes and improvements 2026-02-16 21:08:06 +00:00
FoxxMD 17b11f19a3 feat(nowplaying): Update now playing based on play presence AND player status 2026-02-16 17:26:34 +00:00
FoxxMD 2993279d78 feat(discord): Add platform specific now playing should update check 2026-02-15 18:36:27 +00:00
FoxxMD b95b7e3781 fix(discord): Add missing applicationId env and docs 2026-02-14 00:12:16 +00:00
FoxxMD c57200132e use discord.js deps in production build 2026-02-13 23:04:38 +00:00
FoxxMD f6d9921581 type fixes 2026-02-13 22:08:46 +00:00
FoxxMD 3aada673d5 feat(discord): Add album art functionality 2026-02-13 22:00:40 +00:00
FoxxMD 1456070044 fix(discord): Better error handling and reconnect logic 2026-02-13 20:42:23 +00:00
FoxxMD 883797a51d docs(discord): Add discord docs 2026-02-13 19:47:18 +00:00
FoxxMD 1b93512b0a feat(discord): artwork fallback and ENV config 2026-02-13 19:47:05 +00:00
FoxxMD 7c99262a79 feat(discord): Initial implementation #468 2026-02-13 18:39:33 +00:00
FoxxMD d10ecb88bb chore(release): Bump version 2026-02-12 13:31:36 +00:00
FoxxMD bb0f8e1a9a fix(spotify): Destructure external_ids from default object #470 2026-02-12 13:18:49 +00:00
FoxxMD 8be1ace9a1 chore(release): Bump version 2026-02-11 15:26:20 +00:00
FoxxMD 80d6879d2a chore: Update git-cliff workflow
* Add as dev dependency with npm script so it can be run anywhere project is installed
* Update config with newer template based on scopesorted
  * Add more scopes and give them pretty emojis
2026-02-11 15:24:54 +00:00
Matt Foxx 0b8dfaa5b5 Merge pull request #466 from FoxxMD/deezerImprovements
feat: Deezer family account and other improvements
2026-02-11 08:17:55 -05:00
Matt Foxx 3e25be1e9d Merge pull request #467 from FoxxMD/jellyfinUnknownLax
feat(jellyfin): More granular media type filtering
2026-02-11 08:17:35 -05:00
FoxxMD 4e550d14af fix(jellyfin): Fix options being required
The old jellyfin config had optional options which kept the new jf options required from *always* being required. Removing the old config revealed this bug.

Fixes #469
2026-02-10 20:06:06 +00:00
FoxxMD eab33559ca fix: allow musicvideo media type and use allowedMediaTypes for NowPlayingItem check 2026-02-10 19:39:35 +00:00
FoxxMD 83076a0770 docs: Update screenshot 2026-02-05 17:21:47 +00:00
FoxxMD db65b3140a docs: Remove erroneous ENV restriction 2026-02-05 00:43:17 +00:00
FoxxMD 70b703df8b docs: Wording and client order
* Make landing description more generalized.
* Put Koito as first client. Maloja is unmaintained and shouldn't be showcased.
2026-02-04 14:15:35 +00:00
FoxxMD 00070a1bd2 docs: Make compose default tab for installation 2026-02-04 13:44:13 +00:00
FoxxMD c7bf419ac4 chore: Remove unused dockerfile
alpine hasn't been used in awhile due to dns problems
2026-02-04 13:41:33 +00:00
FoxxMD 850f9f081a fix(jellyfin): Allow all media types when allowUnknown is true
Fixes #445
2026-02-03 18:51:28 +00:00
FoxxMD 1110b6b047 docs(deezer): Use config tabs 2026-02-03 17:41:18 +00:00
FoxxMD 3c99adc01c feat(deezer): Add family account monitoring #344 2026-02-03 17:26:05 +00:00
FoxxMD 55cec9a16f fix(deezer): Fix error list access #344 2026-02-03 16:04:30 +00:00
FoxxMD c36c7c6440 docs(plex): Add Musicbrainz stage guidance
#447
2026-02-03 15:48:39 +00:00
FoxxMD 7a4e0ea213 fix: Normalize state param for auth redirect URLs
Fixes #443
2026-02-03 15:07:34 +00:00
FoxxMD 5a7e1c0ae5 choer: Bump lastfm-ts-api version
scriptex/lastfm-ts-api#52
2026-02-03 13:40:22 +00:00
Matt Foxx 9c3b079620 Merge pull request #463 from Kernald/oci-source-label
chore(docker): Add source OCI labels
2026-01-31 11:43:26 -05:00
FoxxMD c75fff6833 fix: compatibility for old scrobbles #462 2026-01-31 11:40:14 -05:00
Marc Plano-Lesay 6adf2851ba chore(docker): Add source OCI labels
These labels are used, among other things, by Renovate to pull
changelogs/link to the upstream repository automatically
(https://docs.renovatebot.com/modules/datasource/docker/).
2026-01-31 11:06:08 +11:00
FoxxMD c6e1b9c629 chore: Fix links in issue templates 2026-01-30 10:17:08 -05:00
FoxxMD 09d7bad4a6 chore: Bump version for release 2026-01-30 15:01:47 +00:00
FoxxMD 4e0bc1552a fix(librefm): Relax response content-type
Patch package to fix response content type change from librefm until scriptex/lastfm-ts-api#52 is merged
2026-01-30 14:40:48 +00:00
Matt Foxx 205d626cd6 Merge pull request #460 from FoxxMD/playDiff
feat: Implement play diffing with inputs for debugging
2026-01-29 17:43:49 -05:00
FoxxMD dee2e5ecf8 docs: Add help docs and update github issue template with play debug data 2026-01-29 21:19:36 +00:00
FoxxMD 995f857b08 fix(plex): Do not use album artist MBID when existing artist or is VA #450 2026-01-29 18:58:22 +00:00
FoxxMD 073af2d3b4 fix(plex): Add missing brainz data to lifecycle original 2026-01-29 18:37:13 +00:00
FoxxMD 4b0eaff99c Add missing error capture to dead letter retry failures 2026-01-29 18:18:56 +00:00
FoxxMD 2aac56c97a feat(scrobblers): Serialize error correctly and add debug info to dead letter ui 2026-01-29 17:13:16 +00:00
FoxxMD 67f6880d32 feat(scrobblers): Refactor scrobbling functions to capture transactional data
Capture payload, response/body, errors, and warnings so we can store them in play lifecycle for future debugging
2026-01-29 16:34:25 +00:00
FoxxMD 1f2816e6fb Move debug icon to left side 2026-01-27 17:48:12 +00:00
FoxxMD 50565fe312 fix: Deep clone discovered plays
Fixes shared scrobble object overwriting lifecycle data for each scrobble client
2026-01-27 17:42:23 +00:00
FoxxMD 6332da52e6 Use ms play with scrobble payload for api response 2026-01-27 17:10:18 +00:00
FoxxMD fa06d65046 fix(maloja): Fix logging objects 2026-01-27 17:06:20 +00:00
FoxxMD 1ce27a2543 docs: Comment why compilations don't work for maloja 2026-01-27 17:05:01 +00:00
FoxxMD 406a4b3448 fix: add missing bug icon for copy action in scrobbled page 2026-01-26 20:23:48 +00:00
FoxxMD c0259ac409 feat: Add debug copy action to ui 2026-01-26 20:05:17 +00:00
FoxxMD d474ae24d3 feat: Use json diff to track play changes
* Store Source input and original play
* Store transformer hook results as diff patches alongside stage-specific arbitrary input data
  * Use jsondiffpatch console format to log stage diffs
* Store scrobble payload data
2026-01-26 19:13:03 +00:00
FoxxMD 62b06c0f56 refactor(sonos): Move device/group filters before track type checks to enable easier user workarounds
Device Name/GroupName is more likely to exist and less likely to cause issues than upnpclass and other things we are looking for to verify its a valid track.

User can block the erroring device to "fix" bad behavior.
2026-01-26 13:46:21 +00:00
FoxxMD af6851562c fix(sonos): Check for UpnpClass existing #398 2026-01-26 13:38:19 +00:00
FoxxMD cb62a6a659 fix(musicbrainz): Check for undefined status #458 2026-01-24 19:24:48 +00:00
Matt Foxx 7411f7324a Merge pull request #456 from FoxxMD/sonosStateHandle
fix(sonos): Gracefully handle failure when fetching device state #455
2026-01-23 13:33:54 -05:00
FoxxMD b3467e97b7 fix(sonos): Gracefully handle failure when fetching device state #455 2026-01-23 17:43:03 +00:00
FoxxMD 5b83e774e6 fix: TS typings for brainz meta and missed recording-track rename 2026-01-23 17:09:25 +00:00
FoxxMD 9cc2ca1010 docs: Refactor lastfm source instructions in quickstart guide to use ENV 2026-01-22 21:43:50 +00:00
FoxxMD 77aaefa3c3 docs: Update readme 2026-01-22 16:25:13 -05:00
FoxxMD 7a09950312 chore: Bump version for release 2026-01-22 20:17:07 +00:00
FoxxMD 2244eb4bf0 docs(sonos): Update admonitions 2026-01-22 20:10:23 +00:00
FoxxMD 5f9907adf7 chore: Cleanup dynamic import refactor 2026-01-22 14:49:57 +00:00
Matt Foxx 64eeae78a8 Merge pull request #448 from FoxxMD/mbTrack
feat(musicbrainz): Search by track MBID
2026-01-22 09:36:30 -05:00
Matt Foxx a053104b13 Merge pull request #451 from FoxxMD/lastfmDebug
feat(lastfm): Add response content logging
2026-01-22 09:34:26 -05:00
FoxxMD d2ea1adcb0 chore(lastfm): Use updated lastfm package
* Support timeout scriptex/lastfm-ts-api#50
* Error response debugging scriptex/lastfm-ts-api#51
2026-01-22 14:28:08 +00:00
Matt Foxx 20d86905c0 Merge pull request #439 from FoxxMD/sonos
Implement Sonos Source
2026-01-22 09:23:49 -05:00
FoxxMD 5fba6436ab refactor(sonos): Add missing sonos source dynamic usage 2026-01-22 13:51:54 +00:00
FoxxMD dea6d68228 feat(sonos): Better cleanup for for devices 2026-01-22 13:48:44 +00:00
FoxxMD 2ac9547f6a Use device invariant data for iterating devices
* Use invariant hash for seen state logging
  * Removes dependence on individual properties being available
* Drop player state if an existing existing player has the same play, rather than dropping by existing group
* Enable logging empty player for more debugging info

try to get device summary with for loop
2026-01-22 13:48:42 +00:00
FoxxMD 49529af813 feat(sonos): Add group/device filtering 2026-01-22 13:48:42 +00:00
FoxxMD 54eabf49f0 feat(sonos): Fix first media seen logging and add debug guidance to docs
add env gate to sonos test playground
2026-01-22 13:48:39 +00:00
FoxxMD 16ad50ca4d feat: Implement Sonos Source
disable debug test

Add mising type to source status data
2026-01-22 13:48:37 +00:00
Matt Foxx 49d09d0420 Merge pull request #453 from FoxxMD/memoryFix
refactor: Optimize production for memory usage
2026-01-22 08:31:14 -05:00
FoxxMD 50e613748f reactor: try dynamic import for components 2026-01-21 16:51:51 +00:00
FoxxMD 69d76d2d44 refactor: Use compiled json for schema in production
* Use dynamic import for schema based on environment
  * If production use pre-generated json to avoid loading vega generator -- big memory footprint reduction
  * If not production use vega so schema is always up to date
2026-01-21 16:24:58 +00:00
FoxxMD 65d9026f01 docs(tealfm): Update tealfm links 2026-01-21 13:16:21 +00:00
FoxxMD 5b6ad26b2e feat(lastfm): Add response content logging #449 2026-01-20 15:53:25 +00:00
FoxxMD 59a132e536 chore: Add config types to issue template 2026-01-19 21:35:08 +00:00
FoxxMD 9ec9de519e fix(musicbrainz): find correct release when release or track mbid is present 2026-01-19 21:08:10 +00:00
FoxxMD 54c631eb40 Fix missed track to recording rename 2026-01-19 18:24:59 +00:00
FoxxMD ef96241743 feat(musicbrainz): Search by track MBID using basicorids #447 2026-01-19 18:18:13 +00:00
FoxxMD 0b51c3a5fa refactor(musicbrainz): Rename recording mbid property to recording
So that introducing trackId isn't confusing
2026-01-19 18:09:29 +00:00
FoxxMD 9ccdd53bd8 feat: Implement ENV config for clients as sources
Source that are also clients did not have ENV config due to early design choices that are now irrelevant.

Implement ENV config for these source to make config easier for everyone.
2026-01-19 15:45:25 +00:00
Matt Foxx 8e6730a5ce Merge pull request #444 from FoxxMD/prometheus
feat: Add prometheus metrics #438
2026-01-18 18:02:24 -05:00
FoxxMD a2f4c3088a fix(prometheus): Set content type to plain text #438 2026-01-18 16:39:01 +00:00
FoxxMD ecb65a29c1 docs: Add prometheus usage 2026-01-16 21:16:55 +00:00
FoxxMD b4d8b9ad37 refactor: Add type label for prometheus metrics 2026-01-16 21:16:47 +00:00
FoxxMD d3cde0e373 feat: Add prometheus metrics #438 2026-01-16 21:03:23 +00:00
FoxxMD 28667067ca docs: Add missing default preset to quickstart MB instructions 2026-01-15 21:04:20 +00:00
FoxxMD c469ef02dd docs: Add librefm to repo readme 2026-01-15 15:50:20 +00:00
FoxxMD f48990e31f chore: Bump version for release 2026-01-15 15:39:55 +00:00
FoxxMD 2a0733968e fix: Fix koito spelling 2026-01-14 20:05:59 -05:00
FoxxMD cc7c90c012 docs: Fix styling on default details 2026-01-14 14:19:58 +00:00
FoxxMD 02a54ce8bc fix(librefm): Log warning for empty account rather than throwing
#401
2026-01-14 13:29:56 +00:00
Matt Foxx 25256321b2 Merge pull request #440 from FoxxMD/docComponents
Doc components
2026-01-13 15:42:43 -05:00
FoxxMD 370e3e90b0 docs: Details Admonotions for main configuration 2026-01-13 20:40:04 +00:00
FoxxMD c0c449ecc2 docs: Use AdmonitionDetails 2026-01-13 20:35:11 +00:00
FoxxMD 4f9cfcf46d docs: Re-style details and add custom admonition details component 2026-01-13 20:05:23 +00:00
Matt Foxx b05ff9814e feat: Librefm Source/Client (#436)
* feat(librefm): Implement librefm
* feat(librefm): Make api key/secret optional
* feat(librefm): Add LIBREFM_ENABLE env to allow enabling librefm with no other options
* feat(ui): Show client/source hint for all ambiguous components
2026-01-13 08:24:31 -05:00
Matt Foxx fa34e83474 Merge pull request #437 from jackwilsdon/add-undici
fix: add missing undici dependency
2026-01-12 11:07:00 -05:00
Jack Wilsdon 1c73dd5efa fix: add missing undici dependency 2026-01-11 22:22:28 +00:00
FoxxMD 8f66914dfb chore!: Remove deprecated Plex/Tautulli Sources 2026-01-08 19:00:56 +00:00
FoxxMD 424a764a9c chore!: Remove deprecated Jellyfin Source 2026-01-08 18:45:09 +00:00
FoxxMD 53ef315f50 feat(musicbrainz)!: Improve ENV presets
* Adds `fields` and `id` presets to give ENV users more flexibility
* BREAKING: Removes searchOrder from `sensible` so users must now use `default` with presets (other than `sensible`) to get default searchOrder methods added
2026-01-08 15:18:23 +00:00
FoxxMD 9e3c15c9e8 Improve logging for artist search method 2026-01-08 13:55:33 +00:00
FoxxMD 240fb2ca7d docs: Add read-only install instructions
Closes #429 thanks @drake7707
2026-01-08 13:33:42 +00:00
Matt Foxx 4657df2900 Merge pull request #433 from FoxxMD/musicbrainzSearchAgnostic
feat(musicbrainz)!: Add more control, and types, to api search execution
2026-01-08 08:14:53 -05:00
FoxxMD a3aa0b9988 docs: Improve MB docs 2026-01-07 22:50:01 +00:00
FoxxMD c33ed61220 fix(musicbrainz): Fix default assignment for searchOrder
Prevent undefined so in component transform options from causing defaults to override stage defaults
2026-01-07 22:50:01 +00:00
FoxxMD d7cf3f82ee fix(musicbrainz): Check for release data type before logging #434 2026-01-07 22:50:01 +00:00
FoxxMD 1ec492725c feat(musicbrainz): Implement field scoring 2026-01-07 22:49:57 +00:00
FoxxMD 03d9dc36ec fix: Remove cache assignment in child class 2026-01-07 22:47:22 +00:00
FoxxMD f0ffa1ad8a feat(transformer): Improved caching
* Cache full transform hooks and omit logging when cache hit
  * Big logging reduction for sources that return all plays (teal, lastfm, etc.)
* Implement new error type for stages to discriminate between expected prerequisite failures and true exceptions
  * Used to determine logging level and if hook should be cached
2026-01-07 22:47:22 +00:00
FoxxMD 528d269dde fix: Don't mutate original error when merging cause stack for logging 2026-01-07 22:47:22 +00:00
FoxxMD 5a642c4c06 fix some wording 2026-01-07 22:47:22 +00:00
FoxxMD 867f382cc0 fix type for env preset 2026-01-07 22:47:22 +00:00
FoxxMD 203411a875 docs(musicbrainz): Update search methods 2026-01-07 22:47:22 +00:00
FoxxMD 9833c6c7bb feat(musicbrainz): Implement basic-or-ids search 2026-01-07 22:47:22 +00:00
FoxxMDandgabe 4af0200161 feat(musicbrainz): add exclusive search by recording MBID
* adds mbidrecord search type

Co-authored-by: gabe <gabeklav@fastmail.com>
2026-01-07 22:47:22 +00:00
FoxxMD d27ec5f031 feat(musicbrainz)!: Replace fallback search with searchOrder for more search control
* search order and type of searches now determined by `searchOrder` instead of multiple `fallback*` options with hardcoded order
* Deprecate `fallback*` options
* Deprecate `fallbackArtistSearch` for `searchArtistMethod`
* Refactor ENV presets to use new options
2026-01-07 22:47:22 +00:00
Matt Foxx 9482b66846 Merge pull request #435 from FoxxMD/fixPreTransformClient
fix: Move preCompare transform hook for clients to the correct place …
2026-01-07 17:45:52 -05:00
FoxxMD dcf35a6930 more test fixes 2026-01-07 14:07:03 +00:00
FoxxMD e526fe79eb fix tests 2026-01-07 13:58:03 +00:00
FoxxMD ad61d29f08 fix: Move preCompare transform hook for clients to the correct place in lifecycle 2026-01-07 13:45:43 +00:00
Matt Foxx 2485267666 Merge pull request #426 from owendaprile/push-ursmqpqmrwns
feat(plex): Retrieve MusicBrainz IDs
2026-01-05 15:47:17 -05:00
FoxxMD 46f4fbe585 fix(endpointlz): Mark play as new from source
May fix scrobble clients not refreshing when scrobbling old/offline plays #430
2026-01-05 18:40:51 +00:00
FoxxMD a7a99eec05 feat(plex): Add reasonable timeout to cache calls 2026-01-05 18:15:26 +00:00
FoxxMD bad960d786 refactor(plex): Use existing cache and simplify failure result
* Use Keyv memory cache
* Return undefined instead of null to simplify assignment
* Throw error with cause for clearer logging
2026-01-05 18:10:32 +00:00
FoxxMD a3110dbddc fix(mpd): Add missing MPD envs to source building #432 2026-01-05 17:29:14 +00:00
FoxxMD 01c95d79c1 fix(musicbrainz): Simplify config parsing and log original ids
* simplify config building so new properties aren't accidentally not assigned
* add logPreMbid to defaults to make id search troubleshooting easier
2026-01-05 17:16:01 +00:00
FoxxMD 2b7cbc41f3 feat(musicbrainz): Improved missing mbid parsing and tests 2026-01-05 16:54:48 +00:00
FoxxMD 8bf1bffd70 fix(tealfm): Don't define brainz artist array if empty 2026-01-05 16:21:31 +00:00
FoxxMD df163a26de test(lastfm): Improve artist and mbid parsing
* Update scrobble types to more accurate represent what lastfm returns
* Add tests to check mbids and artists are parsed correctly
2026-01-05 15:29:48 +00:00
FoxxMD a98f8448d6 chore: Bump version for release 2025-12-31 20:47:17 +00:00
FoxxMD 541892a59e fix(musicbrainz): Don't assume duration is missing 2025-12-30 19:28:39 +00:00
FoxxMD 8dcb6528ac fix: Convert MB length to seconds
#423 #427
2025-12-28 16:03:01 +00:00
Owen D'Aprile 210f9a75ed feat(plex): Cache MusicBrainz IDs 2025-12-23 17:23:55 -05:00
Owen D'Aprile 50e3a3bbfe feat(plex): Retrieve MusicBrainz IDs 2025-12-23 17:23:49 -05:00
Matt Foxx 7dd238a48a Merge pull request #424 from SilverKnightKMA/patch-1
Rename 'unnamed-koito' to 'unnamed-rocksky'
2025-12-21 23:25:23 -05:00
Vũ Thanh Tùng 8a157947a6 Rename 'unnamed-koito' to 'unnamed-rocksky' 2025-12-22 10:54:51 +07:00
FoxxMD 5e309250f5 docs: Add valkey docker compose connection example 2025-12-18 16:48:19 +00:00
FoxxMD 0875aefacf feat(tealfm): Implement service filtering and parse isrc/music service from records 2025-12-17 16:34:48 +00:00
FoxxMD 083dcc494e refactor: Move ISRC to main play data
It's not a brainz-specific identifier
2025-12-17 16:09:45 +00:00
FoxxMD e5105b1bb2 chore: Bump version for release 2025-12-17 14:01:19 +00:00
FoxxMD e998a6d95a docs: Add debug mode logging hint 2025-12-17 14:00:16 +00:00
FoxxMD 98f045a70f docs: Improve general transform docs 2025-12-17 13:57:56 +00:00
FoxxMD 419f7cf657 docs(musicbrainz): Highlight ENV config 2025-12-17 13:45:49 +00:00
Matt Foxx 9bef0791c4 Merge pull request #421 from FoxxMD/secretsInterpolation
Secrets interpolation
2025-12-16 21:00:53 -05:00
FoxxMD 15d667f653 docs: Better warnings for ENV interpolation 2025-12-17 01:59:05 +00:00
FoxxMD 939ae6f892 docs: Add config interpolation docs 2025-12-17 01:52:07 +00:00
FoxxMD 0cd36e57d6 feat: Log as WARN if there are unmatched interpolation keys
#420
2025-12-17 01:30:52 +00:00
FoxxMD 5acb92e696 feat(musicbrainz): Search by ISRC before anything else
If ISRC is presents then it should be a guaranteed hit and match. Do a "pre" search using only ISRC before doing the normal field search.
2025-12-17 00:31:22 +00:00
FoxxMD 10a90c7d48 fix(musicbrainz): Add missing duration logic 2025-12-16 23:59:31 +00:00
FoxxMD f0c5686b4a fix: Parse non-interpolated json data as data 2025-12-16 23:53:38 +00:00
FoxxMD cae914b21b docs: Better language for mb quickstart section 2025-12-16 21:19:26 +00:00
Matt Foxx 344083a3a9 Merge pull request #419 from FoxxMD/spotifyBrainzMeta
feat: Add additional metadata from spotify and for listenbrainz submissions
2025-12-16 15:54:18 -05:00
FoxxMD 36ed40f628 feat: Implement secret interpolation from ENV
#420
2025-12-16 20:53:00 +00:00
FoxxMD c650c4a1e0 feat: Implement string interpolation from object values 2025-12-16 20:32:04 +00:00
FoxxMD f50c3b40d5 docs: Add musicbrainz step to quickstart 2025-12-16 19:29:06 +00:00
FoxxMD c1318f7efc feat: Add additional metadata from spotify and for listenbrainz submissions
* Parse irsc and track number from spotify data
* Add isrc and track number to listenbrainz submission
* Correct spotify ids for listenbrainz submission

#418
2025-12-16 18:18:30 +00:00
FoxxMD 615947b4d2 fix(transforms): Update types to allow json validation without type for user
#267
2025-12-16 16:36:47 +00:00
Matt Foxx 7c0a9fcd9a Merge pull request #417 from FoxxMD/improvedStageFlowControl
Improved stage flow control
2025-12-16 09:57:16 -05:00
FoxxMD 72572c9397 docs(transformers): Add Flow Control 2025-12-16 14:54:02 +00:00
FoxxMD 0e3874f35f chore: Update wording for disable web 2025-12-16 13:33:53 +00:00
FoxxMD db8a289517 feat(transformers): Log more transform stage detail for components 2025-12-16 13:33:53 +00:00
FoxxMD 698f0b91fc feat(transformers): Add onSkip flow control
* Don't use newtransform when nothing has been assigned
* Remove nanoid accidental commit
2025-12-16 13:33:23 +00:00
FoxxMD fb08a63e6f fix(musicbrainz): Throw errors on pre/post fetch so onFailure works as expected
#414
2025-12-15 18:37:26 +00:00
FoxxMD a320aba4da fix(transformer): Improve stage error logging
* Merge causes if a simple error exists in the stack, reduces stack logging noise
* Log top level message and print error instead of duplicating message in another error
* Fix adding transform history for failed-continue stage
2025-12-15 18:36:41 +00:00
FoxxMD 088e26ce4f feat(musicbrainz): Make fallback album search configurable #414 2025-12-13 22:23:41 +00:00
Matt Foxx e273159ecb Merge pull request #413 from FoxxMD/spotifyNonItem
fix(spotify): Don't report player state if spotify api doesn't return an item
2025-12-12 16:33:38 -05:00
FoxxMD f0ac53ffff fix(spotify): Don't report player state if spotify api doesn't return an item
It's not useful to the stateful player to return a player state without an item

#412
2025-12-12 21:14:09 +00:00
FoxxMD 4a364ce766 chore: Bump version for release 2025-12-12 13:59:33 +00:00
FoxxMD 6c4cd27aa0 fix(transformer): Don't try to build env transformer when no ENV exists 2025-12-12 13:36:51 +00:00
FoxxMD 861578a417 fix(musicbrainz): Wrong default argument for preRequest 2025-12-12 13:30:46 +00:00
FoxxMD 7f62aa79d0 docs(transforms): Add ENV Config quick example 2025-12-11 18:32:49 +00:00
FoxxMD 7223d51690 chore: Bump version for release 2025-12-11 16:52:28 +00:00
FoxxMD 7eabd7f4df fix(tealfm): Allow DID identifier 2025-12-11 16:51:47 +00:00
FoxxMD 88b81dcd12 docs(tranforms): Add default stage 2025-12-11 16:27:23 +00:00
FoxxMD 5d6b3d55cc feat(musicbrainz): Add configurable timeout 2025-12-11 16:00:10 +00:00
FoxxMD a5f4f1be6e docs: Add transform snippets to landing page 2025-12-11 15:35:48 +00:00
FoxxMD ac329cf538 docs(musicbrainz): Additional servers 2025-12-11 15:26:10 +00:00
FoxxMD fafd6a09a0 docs: Improve caching guidance 2025-12-11 15:16:28 +00:00
FoxxMD 759029107a fix(musicbrainz): Fix artist grouping
Remove parenthesis around AND making clause required
2025-12-11 14:08:34 +00:00
FoxxMD 802484c1bb feat(musicbrainz): Implement apikey header 2025-12-11 01:31:11 +00:00
FoxxMD 4dfb485c87 chore: Bump logging for bug fix 2025-12-11 01:21:18 +00:00
FoxxMD 57c1812547 fix(transformer): Fix missing else block when no transform history has occurred 2025-12-11 01:16:59 +00:00
FoxxMD c261699f3e feat(musicbrainz): Reduce retry limit with patched package 2025-12-11 01:16:39 +00:00
FoxxMD 039deca865 feat(musicbrainz): Implement host fallback if multiple are available 2025-12-10 16:20:31 +00:00
FoxxMD f8a18dd1ee test(musicbrainz): Add basic sanity test 2025-12-10 15:07:21 +00:00
FoxxMD de93fe166b refactor(musicbrainz): Move api init and caching closer to api client
* Move client init into MS api client for more control over options used
* Move caching into api call
* Cache URL using async storage
2025-12-10 14:43:29 +00:00
FoxxMD 1956484295 docs: More musicbrainz info 2025-12-09 17:10:56 +00:00
FoxxMD f76604aca5 docs: Add more musicbrainz stage info 2025-12-09 16:56:02 +00:00
FoxxMD 1f01d3e7be docs: Add more sources in quickstart 2025-12-09 16:49:29 +00:00
Matt Foxx eae1a84832 Merge pull request #406 from FoxxMD/musicbrainzTransform
Musicbrainz transformer
2025-12-08 14:09:30 -05:00
FoxxMD 17baebdbf2 feat(transformers): Implement applyng limited transform configs via ENV 2025-12-08 19:02:54 +00:00
FoxxMD 023bd74f12 docs(transformer): Add freetext search 2025-12-08 17:30:24 +00:00
FoxxMD 021ce4d03d test: freetext fallback 2025-12-08 17:08:56 +00:00
FoxxMD 761c0f79fb feat(transformer): Add freetext fallback search for musicbrainz api 2025-12-08 17:04:30 +00:00
FoxxMD 5e63936dda fix: if 2025-12-08 13:11:03 +00:00
FoxxMD a7134e2dae fix(transformer): Missing fallbacksearch assignment 2025-12-07 20:10:12 +00:00
FoxxMD 1289c53941 docs: Update musicbrainz and add additional parts to general transform docs 2025-12-05 20:54:52 +00:00
FoxxMD b4a36a42cd feat(transform): Always log transform diff if in debug mode 2025-12-05 20:48:37 +00:00
FoxxMD ea4d9b3764 fix(transformers): Add missing parts parsing 2025-12-05 19:25:38 +00:00
FoxxMD 5c2819a70c test(tranformer): Add longer timeout and fix logger used in musicbrainz suite 2025-12-05 19:20:31 +00:00
FoxxMD 83eb7a1b65 fix(transformer): Fix optional option object 2025-12-05 19:20:05 +00:00
FoxxMD 8976bb99f5 fix(transformer): Return stage name for proper diff logging 2025-12-05 19:19:50 +00:00
FoxxMD 40b5ef594a fix: revert lastfm debug code 2025-12-05 19:18:39 +00:00
FoxxMD 5c6e2b8cad feat(transformer): Add musicbrainz api url logging 2025-12-05 19:10:08 +00:00
FoxxMD 8083696469 feat(transform): Add proper support for meta and duration transform parts 2025-12-05 18:47:57 +00:00
FoxxMD c07b572abb feat(transformer): Allow fallback musicbrainz search to use aggressive native parsing 2025-12-05 18:30:11 +00:00
FoxxMD 0972a41962 feat(transformer): Try naive split on artist for additional search 2025-12-05 17:51:05 +00:00
FoxxMD aeae449abf test(transformer): Add working non-ascii example 2025-12-05 17:26:04 +00:00
FoxxMD fa6cc13c86 tests: Add edge case musicbrainz tests
Need to have MB_TEST=true in environment to run them
2025-12-05 17:09:24 +00:00
FoxxMD 3e57b006d2 fix(transformer): Escape lucene specia characters when searching musicbrainz
https://github.com/FoxxMD/multi-scrobbler/pull/406#issuecomment-3615254005
2025-12-05 16:32:41 +00:00
FoxxMD 949c1f9176 revert for netlify test 2025-12-04 20:42:58 +00:00
FoxxMD 2bcd0aa858 test commit for netlify 2025-12-04 20:38:57 +00:00
FoxxMD 8ba8a054a6 docs: Add musicbrainz stage 2025-12-04 20:03:02 +00:00
FoxxMD 08301d8664 mysteriously deleted interface? 2025-12-04 18:00:15 +00:00
FoxxMD c36ffa5ffd more bug fixes 2025-12-04 17:52:48 +00:00
FoxxMD 0d02215f71 More forgiving filter/comparison for maybe values 2025-12-04 17:42:58 +00:00
FoxxMD dadea4b90c feat(transformer): Implement country filter/priority for musicbrainz 2025-12-04 17:22:25 +00:00
FoxxMD 9c89142c47 feat: Improve album artist parsing for musicbrainz data
* Change meta to string array
* Compare artist ids
* Add transformer option to ignore various artists
2025-12-04 17:12:22 +00:00
FoxxMD e055e0bd53 feat(transform): Implement more exhaustive, human-readable transform diff 2025-12-04 17:11:15 +00:00
FoxxMD 14c8730435 Handle album artists 2025-12-04 02:54:34 +00:00
FoxxMD 1410e3a952 fix leftover method from refactor 2025-12-03 21:47:34 +00:00
FoxxMD 1e558de5e1 feat(transformer): Implement filtering and sorting of fetched musicbrainz recordings 2025-12-03 20:49:57 +00:00
FoxxMD dab2414a92 chore: Update musicbrainz-api to use ratelimit
Borewit/musicbrainz-api#1115
2025-12-03 19:12:07 +00:00
FoxxMD 03bb1e0552 fix: Use pmap for all bulk transformer 2025-12-03 17:04:58 +00:00
FoxxMD 1eab06c51d recfactor(transform): Use async hooks for logging to identify transform
Reduces logging noise
2025-12-03 17:04:42 +00:00
FoxxMD d217c91a30 Add musicbrainz-api patch for rate limit 2025-12-03 14:49:49 +00:00
FoxxMD b09cbb4bb5 feat(transformer): Implement staggered api calls to respect rate limit 2025-12-02 21:20:34 +00:00
FoxxMD d6d0ce515f fix score usage 2025-12-02 18:59:50 +00:00
FoxxMD db34c33c42 Fix name usage 2025-12-02 18:59:44 +00:00
FoxxMD 2024454f5b feat(transformers): More improvement to musicbrainz
* Implement multiple endpoints for API client with round robin load balancing
* Implement caching responses
* Add transformer options for search triggers
* Implement transformer checks based on search triggers and score
2025-12-02 18:32:50 +00:00
FoxxMD 3718def8b4 fix(transformer): Fix logging for delimiters 2025-12-01 21:59:38 +00:00
FoxxMD 094071b0ba feat: Implement initial musicbrainz api client 2025-12-01 21:58:32 +00:00
Matt Foxx ca8d397653 Merge pull request #404 from FoxxMD/transformProvider
feat: Implement transform pipeline providers
2025-12-01 13:10:44 -05:00
FoxxMD 6ac44397d0 docs: Refactor transformer docs with stages 2025-12-01 18:04:42 +00:00
FoxxMD d6c4d7cf25 refactor(transforms): Implement per-stage defaults with override 2025-12-01 15:16:24 +00:00
FoxxMD 04ae651b15 fix(plex): Fix library filter when media has no library 2025-11-29 21:06:00 +00:00
FoxxMD ef6e2b0f88 refactor(transformer): Add monolithic artist option and make artist string parsing optional 2025-11-29 05:06:26 +00:00
FoxxMD b7271ea4c2 feat(transformer): Allow configuring artist parsing string sources 2025-11-29 04:51:06 +00:00
FoxxMD b5e835a0c6 fix: Don't throw on nonimplemented notify for transformers 2025-11-29 04:36:17 +00:00
FoxxMD a7df5a8d30 feat: Add flow control to transforms and fix bugs 2025-11-28 23:18:09 +00:00
FoxxMD 883050e0b4 feat: Add native transformer options for delimiters and artist exceptions
#364
2025-11-28 23:18:09 +00:00
FoxxMD b6417ed780 Implement initial native transformer #364 2025-11-28 23:18:09 +00:00
FoxxMD affb16fc0e feat: Implement user transformer and refactor components to use async 2025-11-28 23:17:13 +00:00
FoxxMD 56409dd1d7 feat(transformer): Implement User Transformer class 2025-11-28 23:17:13 +00:00
FoxxMD 1af0193636 feat(transform): Implement abstract transformer
#365 #363 #364
2025-11-28 23:17:13 +00:00
FoxxMD 53e766318c refactor: Move abstract init into own abstract class
So that it can be used for transformer. Component (client/source) inherit from init class
2025-11-28 23:17:13 +00:00
FoxxMD c5269791af docs: Fix kitchensink links 2025-11-23 19:22:21 +00:00
FoxxMD fb5ffd053d fix(lastfm): Fix network error default value 2025-11-21 16:16:05 +00:00
Matt Foxx 0c04e7b79f Merge pull request #366 from FoxxMD/transformPipeline
feat: transform pipeline
2025-11-20 14:35:56 -05:00
FoxxMD dc5fab6f9a Refactor metadata stage so atomic parts are objects or booleans 2025-11-20 18:34:23 +00:00
FoxxMD 128c48947e feat(transforms): Allow for config with stage type
As long as it conforms to user stage type. Preserve backwards compatibility
2025-11-20 17:36:03 +00:00
FoxxMD 4cab02e88d fixed tests and isArray arg 2025-11-20 15:26:45 +00:00
FoxxMD a360687264 first attempt at folding in stages
Not working. Tests fail and compiler has errors.
2025-11-20 15:11:46 +00:00
FoxxMD 6a677b6fe1 refactor: Move transform typings to own file 2025-11-20 15:11:46 +00:00
FoxxMD 5c418d5618 chore(tealfm): Round duration for good measure 2025-11-19 17:25:51 +00:00
FoxxMD e6e782dec8 fix(rocksky): Round duration so value is an integer instead of float
#400
2025-11-19 14:44:04 +00:00
FoxxMD ab7d20d7d3 docs(rocksky): Add scrobbling guarantee disclaimer 2025-11-18 15:15:29 +00:00
FoxxMD ec4c2cdd56 fix(rocksky): Increase scrobble delay to respect PDS operation limits 2025-11-18 14:45:11 +00:00
FoxxMD 88d3e13b52 feat(rocksky): Use duration_ms instead of duation 2025-11-17 15:49:27 +00:00
FoxxMD 7bf70d1973 fix(rocksky): Use correct URL for auth validation 2025-11-17 15:48:16 +00:00
FoxxMD 8eae2dc69d chore: Bump version for release 2025-11-17 15:23:15 +00:00
FoxxMD aa44a08e9e fix(rocksky): Fix wrong property used for api auth header 2025-11-17 15:22:46 +00:00
FoxxMD 6071abca49 docs: Update rocksky link to main page rather than docs 2025-11-13 12:56:22 -05:00
FoxxMD 86a719ba4a docs: Add new clients to repo readme 2025-11-13 17:18:20 +00:00
FoxxMD 39b6f6e602 docs: Fix kitchensink link 2025-11-13 17:16:17 +00:00
FoxxMD bfce5dcc54 chore: Bump version for release 2025-11-13 17:14:36 +00:00
FoxxMD 18537e4e08 docs: Add subsonic guidance for lz scrobbling 2025-11-13 17:13:45 +00:00
FoxxMD 30db8712fc docs: Add redirects for old pages 2025-11-13 17:08:30 +00:00
Matt Foxx 1701a5e5f1 Merge pull request #397 from FoxxMD/lzParsingCleanup
refactor: Cleanup and consolidate listenbrainz listen parsing
2025-11-13 11:46:18 -05:00
FoxxMD 406c39b6b2 fix(tealfm): Fix scrobble play date context 2025-11-13 16:44:56 +00:00
FoxxMD ce7ad36db4 refactor(listenbrainz): Consolidate lz endpoint submit parsing into lz response parsing
Consolidtes logic for parsing lz listen data structure
2025-11-13 16:25:06 +00:00
FoxxMD fa4bf5ab10 refactor(listenbrainz): Move parsing functions out of static 2025-11-13 15:43:44 +00:00
FoxxMD 83d9af2aea fix(listenbrainz): Fix interfaces for submit/listen and consolidate 2025-11-13 15:36:17 +00:00
FoxxMD 433761525c fix(rocksky): Add scrobble to deadletter when ignored 2025-11-13 14:00:30 +00:00
FoxxMD f7a7a36d8b docs: Add robots 2025-11-12 16:59:36 +00:00
Matt Foxx 7e375f1531 Merge pull request #392 from FoxxMD/rocksky
feat(rocksky): Implement Rocksky Source/Client
2025-11-12 10:42:02 -05:00
FoxxMD 357397f225 chore: Add markdown extension to devcontainer 2025-11-12 15:36:14 +00:00
FoxxMD 78bf507226 docs: Update docs to include Rocksky Source and updated config 2025-11-12 15:35:39 +00:00
Matt Foxx f892851b60 Merge pull request #396 from FoxxMD/nowPlayingAccuracyImprovement
Now playing accuracy improvement
2025-11-12 10:10:16 -05:00
FoxxMD 4244ddb8c2 fix(listenbrainz): Use duration_ms if duration not provided 2025-11-11 19:54:34 +00:00
FoxxMD 3e67e9189f feat(listenbrainz): Add request body to logging for debugging 2025-11-11 19:50:14 +00:00
FoxxMD de1cbb2aae fix: Improve timing and update frequency to match scrobbler behavior
* Aggressively start Now Playing update (based on reported) but aggressibely end, based on stale/orphaned, so updates are early but not often
* Refactor update thresholds to use scrobble client behavior
  * LFM and LZ use track duration as "time visible" for Now Playing, don't re-update during that time period
  * Add thresholds to logging for easier troubleshooting

#395
2025-11-11 19:49:58 +00:00
FoxxMD db17455b2e fix: Revert accidental debug commit 2025-11-11 15:18:21 +00:00
FoxxMD a7729fbfa6 feat: Make ATProto handle transform usable by RockSky and teal.fm 2025-11-11 15:10:39 +00:00
FoxxMD 771fdc0686 chore: Regenerate schema with rebased results 2025-11-11 14:48:58 +00:00
FoxxMD 8cad2d4efd feat(rocksky): Implement Rocksky Source/Client 2025-11-11 14:47:59 +00:00
Matt Foxx a094a1aa65 Merge pull request #389 from FoxxMD/tealfm
feat: Implement teal.fm as source/client
2025-11-11 09:37:10 -05:00
Matt Foxx f2d45c5622 Merge pull request #386 from FoxxMD/GH-375/subsonicLoop
feat(subsonic): Add repeat detection
2025-11-10 13:15:50 -05:00
FoxxMD a429bccf07 fix(teal): Adding pds with env when it no longer exists 2025-11-07 22:44:02 +00:00
FoxxMD 04d950980d feat(tealfm): Massage handle for users who don't read docs 2025-11-07 22:41:30 +00:00
FoxxMD 7ec90b97d6 docs: Add tealfm to landing page 2025-11-07 22:30:38 +00:00
FoxxMD a4f989c5e4 feat(tealfm): Resolve pds from handle 2025-11-07 22:29:54 +00:00
FoxxMD da4fbd2f27 docs: Fix tealfm source link 2025-11-07 18:56:22 +00:00
FoxxMD b9e6f32500 chore: Update schema 2025-11-07 18:55:40 +00:00
FoxxMD 5c5c653c3b fix: Fix Source/Client data types 2025-11-07 18:55:27 +00:00
FoxxMD a9e7a4bf16 docs(tealfm): Add docs 2025-11-07 18:46:57 +00:00
FoxxMD 1b4baafa67 feat(teal): Change pds viewer web url 2025-11-07 17:35:00 +00:00
FoxxMD 794ef08aa2 feat(tealfm): Api improvements
* Implement PDS check for App auth clients
* Fix error logging for app loging
2025-11-07 17:31:16 +00:00
FoxxMD fa6ed323ff feat(tealfm): Add tealfm Source 2025-11-07 17:17:50 +00:00
FoxxMD 8110e8e9fa refactor(tealfm): Move common functionality to api client 2025-11-07 16:44:41 +00:00
FoxxMD 12a6eec740 test(tealfm): Add example config to pass config tests 2025-11-07 16:40:27 +00:00
FoxxMD 472021dd2c feat(tealfm): Add ENV config 2025-11-07 16:35:39 +00:00
FoxxMD ebb74d1b5c feat: Implement working tealfm client
* Add pds option
2025-11-07 16:31:41 +00:00
FoxxMD ea23a89101 refactor(tealfm): Implement abstract api client with concrete classes for different auth methods 2025-11-07 15:02:56 +00:00
FoxxMD 9062619d38 feat(tealfm): Implement app password auth 2025-11-07 14:51:31 +00:00
FoxxMD 3fb64be54f feat(tealfm): Initial oauth attempt 2025-11-06 17:22:06 +00:00
Matt Foxx 3e083b19f7 Merge pull request #382 from FoxxMD/GH-380/lzEndpoint
fix(endpointlz): Return expected OK response body
2025-11-05 13:22:47 -05:00
Matt Foxx e197c71bbb Merge pull request #388 from FoxxMD/ghWorkflowHarden
ci: Harden workflows using zizmor
2025-11-05 12:35:11 -05:00
FoxxMD 7b36d95dfc ci: Harden remaining workflows using zizmor
* Pin action digests
* Remove template injection where possible
2025-11-05 12:08:46 -05:00
FoxxMD 0427b6e945 try with double quotes 2025-11-05 11:48:00 -05:00
FoxxMD 326d9e58d7 ci: Use zizmor to fix and audit image publish action
* Pin actions to digest
* Replace template substitution with envs
2025-11-05 11:26:41 -05:00
FoxxMD 63c079f08e docs: Improve sources documentation 2025-11-04 19:18:46 +00:00
FoxxMD ff74fd7e4c docs: Update kitchensink 2025-11-04 19:04:17 +00:00
FoxxMD 680f862ee8 docs: Change installation to category doc link 2025-11-04 18:52:42 +00:00
FoxxMD abdd7f0b55 docs: Improved linking on config/landing and configuration type refinement 2025-11-04 18:49:16 +00:00
FoxxMD c017dd734b feat(subsonic): Add basic repeat detection based on listened duration
#375
2025-11-04 16:31:36 +00:00
Matt Foxx ad478adefd Merge pull request #385 from FoxxMD/depCleanup
refactor: frontend dependency cleanup
2025-11-04 10:40:35 -05:00
FoxxMD b11bd52963 chore(frontend): Update sass package and vite config to remove deprecation warnings 2025-11-04 14:36:22 +00:00
FoxxMD 538df6b6d0 refactor(frontend): Migrate tailwind config to v4 2025-11-04 14:17:51 +00:00
FoxxMD 819afcdb8a refactor(frontend): Update tailwind and move generation into vite
* Use vite tailwind plugin instead of postcss
* Update tailwind so it actually works with plugin
* Remove postcss
* Merge vite config into inline vite-express config
2025-11-04 13:55:31 +00:00
FoxxMD a5693f7c03 chore: Remove unused typedoc config 2025-11-04 13:35:02 +00:00
FoxxMD 22eb5b1168 docs: Add missing client flag for example json usage for clients
Fixes #383
2025-11-03 21:54:44 +00:00
FoxxMD 9402caa4ae docs(endpointlz): Add navidrome url setup instructions 2025-11-03 16:56:32 +00:00
FoxxMD 0aff56e891 feat(endpointlz): Add more logging for invalid routes 2025-11-03 16:10:44 +00:00
FoxxMD a5f6aaa7c0 feat(listenbrainz): Use end-of-string token and wildcard route to better log malformed LZ endpoint routes 2025-11-03 16:08:17 +00:00
FoxxMD 4ee503655c fix(endpointlz): Return expected OK response body
Fixes #380
2025-11-03 15:47:24 +00:00
FoxxMD e53a82782c docs: fix lastfm aio name 2025-11-01 03:50:20 +00:00
FoxxMD 020c6815ba docs: Fix readme links 2025-10-31 17:58:46 +00:00
FoxxMD 5281a92e81 docs: A word 2025-10-31 15:56:05 +00:00
Matt Foxx 8577eb44af Merge pull request #381 from tsirysndr/master
Fix typos in Rocksky documentation
2025-10-31 11:46:08 -04:00
Tsiry Sandratraina edd3cf25b5 Fix typos in Rocksky documentation 2025-10-31 17:24:29 +03:00
FoxxMD b17719c868 docs: Fix missing search index 2025-10-31 13:02:30 +00:00
FoxxMD 971ec9d4db feat(jellyfin): Add frontend url override as env #374 2025-10-30 20:20:10 +00:00
Matt Foxx 6607654e8a Merge pull request #379 from FoxxMD/docsRevamp
Docs revamp
2025-10-30 16:16:52 -04:00
FoxxMD 891e591ceb docs: Reorganize doc contents 2025-10-30 19:56:07 +00:00
FoxxMD 2540ed3945 docs: Add client own files 2025-10-30 18:52:59 +00:00
FoxxMD 7ac3f33cd4 docs: Move Source configs to own files 2025-10-30 18:35:58 +00:00
FoxxMD 9b91f6897e Add new Source overview using category 2025-10-30 17:06:39 +00:00
FoxxMD bc62b99401 docs: Add commonly used components to global scope 2025-10-30 17:06:22 +00:00
FoxxMD a5d1cea910 Add some snippers 2025-10-30 15:21:08 +00:00
FoxxMD 3e3473f6a8 chore: Ignore docusaurus build folders when searching 2025-10-30 15:19:14 +00:00
FoxxMD a0bdd15d15 docs: Move landing page to docs to simply url structure
Set docs as base for router so there isn't an additional docs in url
2025-10-30 15:19:01 +00:00
FoxxMD 920eb10c05 docs: Fix rocksky name/link typo 2025-10-30 09:30:14 -04:00
FoxxMD ae7a436758 chore: Bump version for release 2025-10-30 13:18:49 +00:00
FoxxMD 92d246b45a docs: Add rocksky to repo readme 2025-10-30 13:18:36 +00:00
Matt Foxx 0af480ac1a Merge pull request #377 from FoxxMD/GH-376/plexUrlParseFix
fix(plex): Use better url parsing method
2025-10-30 08:47:48 -04:00
Matt Foxx 9f52c2ec67 Merge pull request #369 from gabeklavans/fix/apprise-tags
fix: parse and use the tags in Apprise config
2025-10-29 11:33:05 -04:00
FoxxMD cfb278087e docs: Update listenbrainz json example to include url 2025-10-29 14:50:21 +00:00
FoxxMD 402b0d2cc7 docs: Add Rockysky setup instructions 2025-10-29 14:47:59 +00:00
FoxxMD 67c79f2eb7 fix(plex): Use better url parsing method
Fixes #376
2025-10-29 13:48:47 +00:00
FoxxMD d759b00c7e test: Add more tests for url parsing 2025-10-29 13:43:34 +00:00
Matt Foxx cac8163e58 Merge pull request #374 from wolffshots/feature/optionally-redirect-image-paths
feat(JellyfinApiSource): add option to override the Jellyfin base url
2025-10-24 09:16:21 -04:00
wolffshots 11dbdaf6cc chore(JellyfinApiSource): extract URL replacement to func and add tests 2025-10-23 17:53:51 +02:00
wolffshots ac6b04f492 feat(JellyfinApiSource): add option to override the Jellyfin base url used in the frontend 2025-10-23 17:22:57 +02:00
Matt Foxx 0790da2e0d feat(plex): Enable accepting self-signed certs #372 (#373)
* feat(plex): Enable accepting self-signed certs #372
* docs(plex): Add invalidcert param to plex example config
2025-10-21 09:10:38 -04:00
Matt Foxx dffb1c79ec Merge pull request #370 from h-wb/feat/add-source-as-lb-ms-name
feat(listenbrainz): add source as music service name
2025-10-08 08:27:01 -04:00
h-wb 46cc9f0b6e feat(listenbrainz): add source as music service name 2025-10-07 23:01:59 -04:00
gabe 140e9f0b20 fix(notification): parse and use the tags in Apprise config 2025-09-26 14:51:04 -04:00
FoxxMD 0fe5779c4b chore: Bump for release 2025-09-23 15:48:46 +00:00
FoxxMD 06b1475b63 chore: remove unused object-hash package 2025-09-23 15:48:21 +00:00
Matt Foxx 6bc415120d Merge pull request #359 from FoxxMD/objectHashReplace
fix: Update object hashing function
2025-09-23 11:43:13 -04:00
FoxxMD 7cee9f04cf fix: Update object hashing function
Might be a solution for #358
2025-09-15 17:37:30 +00:00
442 changed files with 49282 additions and 15629 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
version: '3.7'
services:
# Update this to the name of the service you want to work with in your docker-compose.yml file
app:
multi-scrobbler:
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
@@ -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
+7 -3
View File
@@ -5,9 +5,11 @@
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"dockerComposeFile": [
"compose.yml"
// uncomment to add kasm/discord service
//"../deploy/docker-compose.discord.yml"
],
"service": "app",
"service": "multi-scrobbler",
"workspaceFolder": "/workspaces",
@@ -18,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": {
@@ -26,7 +28,9 @@
"extensions": [
"hbenl.vscode-mocha-test-adapter",
"dbaeumer.vscode-eslint",
"unifiedjs.vscode-mdx"
"unifiedjs.vscode-mdx",
"bradlc.vscode-tailwindcss",
"TakumiI.markdowntable"
]
}
},
+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'
+4 -1
View File
@@ -27,4 +27,7 @@ build
.vscode
**/act/*
tmp-*
.env
.env
*.env
.storybook
.stories
+31 -3
View File
@@ -7,7 +7,9 @@ body:
attributes:
label: Please check existing knowledge before opening an issue
options:
- label: I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/docs/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
- label: I have [read the Help docs](https://foxxmd.github.io/multi-scrobbler/help/)
required: true
- label: I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required: true
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required: true
@@ -23,6 +25,16 @@ body:
placeholder: The app should have scrobbled from Spotify but instead it crashed with an error about undefined properties, etc...
validations:
required: true
- type: checkboxes
attributes:
label: What configuration types are you using for multi-scrobbler? Check all that apply.
options:
- label: "[Environmental Variables (ENV)](https://foxxmd.github.io/multi-scrobbler/configuration/?configType=env#configuration-types)"
required: false
- label: "[Individual](https://foxxmd.github.io/multi-scrobbler/configuration/?configType=file#configuration-types) Config Files"
required: false
- label: "[All-In-One](https://foxxmd.github.io/multi-scrobbler/configuration/?configType=aio#configuration-types) Config File"
required: false
- type: dropdown
id: platform
attributes:
@@ -52,8 +64,8 @@ body:
description: |
Please copy and paste any/all relevant from the DEBUG level log output.
Even if your issue does not seem to be shown in the logs you should still add as much detail as possible, a _lack_ of issues in logs may be a good hint during debugging.
Your logs will be automatically formatted into code, do not need use backticks.
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/docs/configuration#debug-mode).
Your logs will be automatically formatted into code. Do not use fenced code blocks (backticks).
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/configuration#debug-mode).
render: shell
placeholder: |
[2025-02-14 12:47:57.170 -0500] VERBOSE: [Init] Config Dir ENV: undefined -> Resolved: CWD/config
@@ -61,6 +73,22 @@ body:
[2025-02-14 12:47:57.258 -0500] INFO : [Init] Version: master
[2025-02-14 12:47:57.259 -0500] INFO : [Init] Generating schema definitions...
[2025-02-14 12:47:59.148 -0500] DEBUG : [App] [Scrobblers] [Maloja - default] Found 100 recent scrobbles
- type: textarea
id: playDebug
attributes:
label: Play Debug Data
description: |
If relevent, please copy and paste all [**Debug Data**](https://foxxmd.github.io/multi-scrobbler/help#copy-play-debug-data) related to the Plays you are having issues with.
Your data will be automatically formatted into code. Do not use fenced code blocks (backticks) in the text area below.
render: shell
placeholder: |
{
"input": {
"addedAt": 1690250024,
"art": "/library/metadata/186983/art/1698213665",
"duration": 243983,
"grandparentArt": "/library/metadata/186983/art/1698213665",
...
- type: textarea
attributes:
label: Additional Context
@@ -7,7 +7,7 @@ body:
attributes:
label: Please check existing knowledge before opening an issue
options:
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required: true
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required: true
@@ -7,7 +7,7 @@ body:
attributes:
label: Please check existing knowledge before opening an issue
options:
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required: true
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required: true
+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"
+22 -7
View File
@@ -12,10 +12,12 @@ jobs:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
node-version: 20
persist-credentials: false
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
with:
node-version: 24
cache: npm
- name: Install app dev dependencies
@@ -26,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
@@ -41,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@v3
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e #4.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Build output to publish to the `gh-pages` branch:
@@ -53,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
@@ -13,7 +13,7 @@ jobs:
permissions:
packages: write
steps:
- uses: dataaxiom/ghcr-cleanup-action@v1
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 #1.0.16
with:
token: ${{ secrets.GITHUB_TOKEN }}
+29 -16
View File
@@ -48,10 +48,13 @@ jobs:
# list all registries to push to and join all non-empty with comma
# https://unix.stackexchange.com/a/693165/116849
# https://stackoverflow.com/a/9429887/1469797
strings=("${{vars.DOCKERHUB_SLUG}}" "${{vars.GHCR_SLUG}}")
strings=("${VARS_DOCKERHUB_SLUG}" "${VARS_GHCR_SLUG}")
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
joined_string=$(IFS=, ; echo "${strings[*]}")
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
# do not use here!
# instead we can specify repo#sha in build-push-action so we only deal with foreign SHA for actually building image
@@ -73,14 +76,14 @@ jobs:
- name: Login to Docker Hub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -89,13 +92,13 @@ jobs:
# metadata extract for docker labels/image names is done in merge job
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
# for caching
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
with:
# specify foreign repo#sha from PR that is used to build image
context: ${{github.event.pull_request.head.repo.clone_url}}#${{github.event.pull_request.head.sha}}
@@ -114,7 +117,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -133,7 +136,7 @@ jobs:
- test
steps:
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
with:
path: /tmp/digests
pattern: digests-*
@@ -141,25 +144,25 @@ jobs:
- name: Login to Docker Hub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
with:
images: |
${{ vars.DOCKERHUB_SLUG }}
@@ -180,24 +183,34 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.DOCKERHUB_SLUG }}@sha256:%s ' *)
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
- name: Create manifest list and push gchr
if: ${{ vars.GHCR_SLUG != '' }}
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.GHCR_SLUG }}@sha256:%s ' *)
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
- name: Inspect image dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
- name: Inspect image ghcr
if: ${{ vars.GHCR_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
combine-and-comment:
@@ -207,7 +220,7 @@ jobs:
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
steps:
- name: Create comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #2.9.4
with:
recreate: true
header: "pr-release"
+36 -19
View File
@@ -62,13 +62,19 @@ jobs:
# list all registries to push to and join all non-empty with comma
# https://unix.stackexchange.com/a/693165/116849
# https://stackoverflow.com/a/9429887/1469797
strings=("${{vars.DOCKERHUB_SLUG}}" "${{vars.GHCR_SLUG}}")
strings=("${VARS_DOCKERHUB_SLUG}" "${VARS_GHCR_SLUG}")
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
joined_string=$(IFS=, ; echo "${strings[*]}")
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Set short git commit SHA
id: appvars
@@ -94,14 +100,14 @@ jobs:
- name: Login to Docker Hub
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -110,13 +116,13 @@ jobs:
# metadata extract for docker labels/image names is done in merge job
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
# for caching
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
with:
build-args: |
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
@@ -129,11 +135,12 @@ jobs:
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
digest="${VARS_DIGEST}"
touch "/tmp/digests/${digest#sha256:}"
env:
VARS_DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -152,7 +159,7 @@ jobs:
- test
steps:
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
with:
path: /tmp/digests
pattern: digests-*
@@ -160,25 +167,25 @@ jobs:
- name: Login to Docker Hub
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
with:
images: |
${{ vars.DOCKERHUB_SLUG }}
@@ -208,21 +215,31 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.DOCKERHUB_SLUG }}@sha256:%s ' *)
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
- name: Create manifest list and push gchr
if: ${{ vars.GHCR_SLUG != '' }}
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.GHCR_SLUG }}@sha256:%s ' *)
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
- name: Inspect image dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
- name: Inspect image ghcr
if: ${{ vars.GHCR_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
+3 -2
View File
@@ -19,11 +19,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
+14
View File
@@ -73,6 +73,7 @@ web_modules/
# dotenv environment variables file
.env
.env.test
*.env
# parcel-bundler cache (https://parceljs.org/)
.cache
@@ -122,10 +123,19 @@ config/mscache
config/yti-*
config/*.cache
*.txt
!robots.txt
.idea/
src/**/**.js
src/**/**.js.map
src/backend/common/schema/*.json
!src/backend/common/schema/aio-client.json
!src/backend/common/schema/aio-source.json
!src/backend/common/schema/aio.json
!src/backend/common/schema/client.json
!src/backend/common/schema/source.json
docsite/static/schemas/*.json
*.bak
*.p8
@@ -138,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()]
});
+13 -1
View File
@@ -1,5 +1,17 @@
{
"mochaExplorer.require": "tsx/esm",
"mochaExplorer.timeout": 1200000,
"mochaExplorer.exit": true
"mochaExplorer.exit": true,
"search.exclude": {
"**/.docusaurus": true,
"**/build": true,
"**/schema": true
},
"tailwindCSS.files.exclude": [
"docsite/**"
],
"files.associations": {
"*.css": "tailwindcss"
},
"tailwindCSS.experimental.configFile": "src/client/index.css"
}
+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.
+7 -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
@@ -90,6 +92,8 @@ RUN npm run build:parallel && rm -rf node_modules && rm -rf docsite/node_modules
FROM base AS app
LABEL org.opencontainers.image.source="https://github.com/FoxxMD/multi-scrobbler"
COPY --chown=abc:abc *.json *.js *.ts index.html ./
COPY --chown=abc:abc patches ./patches
COPY --from=build --chown=abc:abc /app/dist /app/dist
@@ -106,6 +110,8 @@ ARG APP_BUILD_VERSION
ENV APP_VERSION=$APP_BUILD_VERSION
RUN npm ci --omit=dev --no-audit \
# we used compiled schema json for production so this isn't needed
&& npm uninstall ts-json-schema-generator \
&& npm cache clean --force \
&& chown -R abc:abc node_modules \
&& rm -rf /root/.cache
+57 -47
View File
@@ -3,56 +3,66 @@
[![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"
alt="multi-scrobbler logo" width="180" height="180">
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
* Supports scrobbling from many **Sources**
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#azuracast)
* [Deezer](https://foxxmd.github.io/multi-scrobbler/docs/configuration#deezer)
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#google-cast-chromecast)
* [Icecast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#icecast)
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jellyfin)
* [JRiver](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jriver)
* [Kodi](https://foxxmd.github.io/multi-scrobbler/docs/configuration#kodi)
* [Koito](https://foxxmd.github.io/multi-scrobbler/docs/configuration#koito-source)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm-source)
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm-endpoint)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz-source)
* [ListenBrainz (Endpoint)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz-endpoint)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/docs/configuration#maloja-source)
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mopidy)
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpd-music-player-daemon)
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpris)
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/docs/configuration#muikcube)
* [Plex](https://foxxmd.github.io/multi-scrobbler/docs/configuration#plex) or [~~Tautulli~~](https://foxxmd.github.io/multi-scrobbler/docs/configuration#tautulli)
* [Spotify](https://foxxmd.github.io/multi-scrobbler/docs/configuration#spotify)
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/docs/configuration#webscrobbler)
* [VLC](https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc)
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#yamaha-musiccast)
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/docs/configuration#youtube-music)
* Supports scrobbling to many **Clients**
* [Koito](https://foxxmd.github.io/multi-scrobbler/docs/configuration#koito)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/docs/configuration#maloja)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#webhook-configurations) or [healthcheck endpoint](https://foxxmd.github.io/multi-scrobbler/docs/configuration#health-endpoint)
* 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
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/docs/configuration/transforms)
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/docs/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/docs/installation#nodejs)
* 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://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/docs/quickstart)
[**Quick Start Guide**](https://docs.multi-scrobbler.app//quickstart)
<img src="/assets/status-ui.png" width="800">
<img src="/docsite/static/img/status-ui.png" width="800">
**Why should I use this over a browser extension and/or mobile app scrobbler?**
@@ -63,15 +73,15 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
**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/docs/configuration#lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/docs/configuration#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 for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble 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** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, 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:
@@ -81,7 +91,7 @@ Source configurations consist of:
### Client
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, 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:
@@ -90,15 +100,15 @@ Client configurations consist of:
## Quick Start
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/docs/quickstart)
[See the **Quick Start Guide**](https://docs.multi-scrobbler.app/quickstart)
## Installation
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/docs/installation)
[See the **Installation** documentation](https://docs.multi-scrobbler.app/installation)
## Configuration
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration)
[See the **Configuration** documentation](https://docs.multi-scrobbler.app/configuration)
## Usage
@@ -113,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/docs/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/docs/development/dev-common)
[Detailed architecture and development guides for Sources/Clients](https://docs.multi-scrobbler.app/development/dev-common)
## License
-71
View File
@@ -1,71 +0,0 @@
FROM lsiobase/alpine:3.18 as base
ENV TZ=Etc/GMT
RUN \
echo "**** install build packages ****" && \
apk add --no-cache \
avahi \
avahi-tools \
nodejs \
npm \
&& \
echo "**** cleanup ****" && \
rm -rf \
/root/.cache \
/tmp/*
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ARG data_dir=/config
VOLUME $data_dir
ENV CONFIG_DIR=$data_dir
COPY docker/root/ /
WORKDIR /app
FROM base as build
# copy dep/TS config and install dev dependencies
COPY --chown=abc:abc package*.json tsconfig.json ./
COPY --chown=abc:abc patches ./patches
RUN npm ci \
&& chown -R root:root node_modules
#RUN yarn install
COPY --chown=abc:abc . /app
# need to set before build so server/client build is optimized and has constants (if needed)
ENV NODE_ENV=production
RUN npm run docs:install && npm run build && rm -rf node_modules && rm -rf docsite/node_modules
FROM base as app
#COPY --chown=abc:abc package.json yarn.lock ./
COPY --chown=abc:abc package*.json ./
COPY --chown=abc:abc patches ./patches
COPY --from=build --chown=abc:abc /app/dist /app/dist
COPY --from=build --chown=abc:abc /app/src /app/src
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
COPY --from=base /usr/local/bin /usr/local/bin
COPY --from=base /usr/local/lib /usr/local/lib
ENV NODE_ENV=production
ENV IS_DOCKER=true
ENV COLORED_STD=true
# https://stackoverflow.com/a/63640896/1469797
ARG APP_BUILD_VERSION
ENV APP_VERSION=$APP_BUILD_VERSION
RUN npm ci --omit=dev \
&& npm cache clean --force \
&& chown -R abc:abc node_modules \
&& rm -rf node_modules/@types
ARG webPort=9078
ENV PORT=$webPort
EXPOSE $PORT
+1
View File
@@ -0,0 +1 @@
* Attribution for [`default-artwork.png`](/assets/default-artwork.png): https://www.flaticon.com/free-icon/music_15795471
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 KiB

+42 -29
View File
@@ -1,18 +1,20 @@
# configuration file for git-cliff (0.1.0)
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# changelog header
# A Tera template to be rendered as the changelog's header.
# See https://keats.github.io/tera/docs/#introduction
header = """
# Changelog
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
# A Tera template to be rendered for each release in the changelog.
# See https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | replace(from="v", to="") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
## Unreleased
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
@@ -24,6 +26,7 @@ body = """
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{%- endif -%}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
@@ -33,35 +36,45 @@ body = """
{% endif -%}
{% endif -%}
{% endfor -%}
{% endfor %}
{% raw %}\n{% endraw %}\
{% endfor %}\n
"""
# remove the leading and trailing whitespaces from the template
trim = true
# changelog footer
# A Tera template to be rendered as the changelog's footer.
# See https://keats.github.io/tera/docs/#introduction
footer = """
<!-- generated by git-cliff -->
"""
# Remove leading and trailing whitespaces from the changelog's body.
trim = true
[git]
# allow only conventional commits
# https://www.conventionalcommits.org
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = true
# regex for parsing and grouping commits
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = true
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
{ message = "^feat", group = "⛰️ Features" },
{ message = "^fix", group = "🐛 Bug Fixes" },
{ message = "^doc", group = "📚 Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "🚜 Refactor" },
{ message = "^style", group = "💅 Styling" },
{ message = "^test", group = "🧪 Testing" },
{ message = "^ci", group = "🧑‍🍳 Continuous Integration" },
{ message = "^build", group = "🐋 Build" },
{ message = "^chore\\(release\\)", skip = true },
{ message = "^chore", group = "⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "🛡️ Security" },
]
# filter out the commits that are not matched by commit parsers
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# Order releases topologically instead of chronologically.
topo_order = false
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "oldest"
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MS",
"enable": true,
"data": {
"applicationId": "8190211179716453570",
"ipcLocations": ["192.168.0.105:6655"]
}
}
]
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MS",
"enable": true,
"data": {
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
"applicationId": "8190211179716453570"
}
}
]
+2 -1
View File
@@ -16,7 +16,8 @@
"librariesAllow": ["GoodMusic"],
"librariesBlock": ["BadMusic"],
"additionalAllowedLibraryTypes": ["musicvideos"],
"allowUnknown": false,
"allowMediaTypes": ["audio"],
"frontendUrlOverride": "https://myJellyfinExternal.tld"
},
"options": {
"logPayload": true,
+22
View File
@@ -0,0 +1,22 @@
[
{
"name": "myLibreFmClient",
"enable": true,
"configureAs": "client",
"data": {
"apiKey": "anything",
"secret": "anything",
"redirectUri": "http://localhost:9078/librefm/callback"
}
},
{
"name": "myLibreFmSource",
"enable": true,
"configureAs": "source",
"data": {
"apiKey": "anything",
"secret": "anything",
"redirectUri": "http://localhost:9078/librefm/callback"
}
}
]
+4 -2
View File
@@ -5,7 +5,8 @@
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
"username": "FoxxMD",
"url": "https://api.listenbrainz.org"
}
},
{
@@ -14,7 +15,8 @@
"configureAs": "source",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
"username": "FoxxMD",
"url": "https://api.listenbrainz.org"
}
}
]
+2 -1
View File
@@ -15,7 +15,8 @@
},
"options": {
"logPayload": true,
"logFilterFailure": "debug"
"logFilterFailure": "debug",
"ignoreInvalidCert": false
}
}
]
-17
View File
@@ -1,17 +0,0 @@
[
// DEPRECATED, use API Source instead
// rename files to plex.json to use
{
"name": "MyPlex",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
}
}
}
]
+18
View File
@@ -0,0 +1,18 @@
[
{
"name": "myRockskyClient",
"configureAs": "client",
"data": {
"key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",
"handle": "alice.bsky.social"
}
},
{
"name": "myRockskySource",
"configureAs": "source",
"data": {
"key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",
"handle": "alice.bsky.social"
}
}
]
+13
View File
@@ -0,0 +1,13 @@
[
{
"enable": true,
"name": "MySonos",
"data": {
"host": "192.168.0.150",
"devicesAllow": ["move","beam"],
"devicesBlock": ["roam"],
"groupsAllow": ["family"],
"groupsBlock": ["bedroom"]
}
}
]
-15
View File
@@ -1,15 +0,0 @@
[
{
"name": "MyTautuilli",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
}
}
}
]
+22
View File
@@ -0,0 +1,22 @@
[
{
"name": "myTeal",
"configureAs": "client",
"data": {
"identifier": "alice.bsky.social",
"appPassword": "twog-phu7-4dhe-y4j3"
}
},
{
"name": "myTealSource",
"configureAs": "source",
"data": {
"identifier": "alice.bsky.social",
"appPassword": "twog-phu7-4dhe-y4j3",
// everything below is optional
"serviceAllow": ["plyr"],
"serviceDeny": ["last.fm"]
}
}
]
+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
}
}
]
+26
View File
@@ -0,0 +1,26 @@
services:
multi-scrobbler:
volumes:
- kasm_run:/run
kasmcord:
container_name: kasmcord
image: kasmweb/discord:1.14.0
ports:
# VNC port to use in web browser
- 6901:6901
shm_size: 512m
environment:
# set a password for VNC access
VNC_PW: ${KASM_PW:-hunter2}
XDG_RUNTIME_DIR: /run/user/1000
volumes:
- kasm_run:/run/user/1000
- kasm_config:/home/kasm-user/.config
user: "0"
entrypoint: sh -c "chmod 700 /run/user/1000 && chown -R kasm-user:kasm-user /run/user/1000 && chown -R kasm-user:kasm-user /home/kasm-user/.config && su kasm-user -c '/dockerstartup/kasm_default_profile.sh /dockerstartup/vnc_startup.sh /dockerstartup/kasm_startup.sh'"
volumes:
kasm_run:
name: kasm_run
kasm_config:
name: kasm_config
+24 -8
View File
@@ -1,6 +1,7 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
title: FAQ
---
## Connection Issues
@@ -33,7 +34,7 @@ First, turn on **debug** logging for multi-scrobbler by setting the environmenta
##### Check Host name and URL
The URLs examples in the [configuration](configuration/configuration.mdx) documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
The URLs examples in the [configuration](/configuration#base-url) documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
EX `http://localhost:9078/api/webscrobbler` -> `http://192.168.0.140:9078/api/webscrobbler`
@@ -60,6 +61,21 @@ If multi-scrobbler is not running on the same machine your browser is on then th
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
### I get errors with `ECONNRESET` `ETIMEDOUT` `ENOTFOUND` `EAI_AGAIN`
These are errors indicating a network issue is occuring between you and the Source/Client.
* `ECONNRESET` means the upstream service unexpectedly closed the connection
* `ETIMEDOUT` means the upstream service did not respond to a request MS
* `ENOTFOUND` means that MS could not find a DNS record for the domain of the upstream service
* `EAI_AGAIN` means that a DNS server did not respond to MS's request to lookup the record for a domain
`ECONNRESET` and timeout issues can occur when you have a poor connection or if the upstream service is overloaded or is generally not behaving well. This is a common occurrence for Listenbrainz/Musicbrainz related requests.
Please check the [Upstream Services Status Page](https://status.multi-scrobbler.app) to independently verify if the service is misbehaving. If the service is up and has good uptime then the issue may be in your network.
`ENOTFOUND` and `EAI_AGAIN` and generally caused by DNS caching (stale records) or misbehaving DNS servers. Though this can happen at the upstream service's end, it is *usually* an issue in your network. If you run a Pihole, AdGaurd, or other DNS solution try flushing the cache for the mishaving domain and check that your DNS application is working correctly.
### Youtube Music fails after some time
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
@@ -70,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/configuration.mdx?ytmAuth=ytt#youtube-music) 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
@@ -80,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/configuration.mdx?ytmAuth=cookie#youtube-music) 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
@@ -108,7 +124,7 @@ Multi-scrobbler works the same was the official Spotify-Last.fm integration work
The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) *should* use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented.
To diagnose these issues you [**must enable payload logging**](configuration/configuration.mdx#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
To diagnose these issues you [**must enable payload logging**](/configuration/sources/google-cast#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
### Google Cast device does not track media
@@ -120,17 +136,17 @@ MS logs will tell you what type the media is reported as with lines like:
My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track
```
Refer to [Allow Unknown Media Type](configuration/configuration.mdx#allow-unknown-media-type) section to fix this
Refer to [Allow Unknown Media Type](/configuration/sources/google-cast#allow-unknown-media-type) section to fix this
```
My Artist - Example Track has 'movie' media type so will not track
```
Refer to [Force Media Tracking](configuration/configuration.mdx#forcing-media-tracking) section to fix this
Refer to [Force Media Tracking](/configuration/sources/google-cast#forcing-media-tracking) section to fix this
### VLC is not scrobbling fields correctly
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](configuration/configuration.mdx#vlc-information-reporting)
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](/configuration/sources/vlc#vlc-information-reporting)
### Youtube Music misses or duplicates scrobbles
@@ -169,7 +185,7 @@ In your YTM configuration (`ytmusic.json`) add `logDiff` under `options` like th
or set either ENVs:
* `YTM_LOG_DIFF=true`
* [`DEBUG_MODE=true`](configuration/configuration.mdx?#debug-mode)
* [`DEBUG_MODE=true`](/configuration#debug-mode)
This will cause MS to log YTM history changes similar to this:
+3 -3
View File
@@ -1,8 +1,8 @@
{
"label": "Configuration",
"position": 4,
"position": 5,
"link": {
"type": "generated-index",
"description": "Configuring Multi-Scrobbler and examples"
"type": "doc",
"id": "configuration/configuration"
}
}
@@ -0,0 +1,7 @@
{
"label": "Scrobble Clients",
"link": {
"type": "doc",
"id": "configuration/clients/clients"
}
}
@@ -0,0 +1,134 @@
---
sidebar_position: 2
title: Overview
description: Scrobble Clients
toc_max_heading_level: 4
---
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
## Clients
| Name | Now Playing |
| :-------------------------------------------------- | :---------- |
| [Discord](/configuration/clients/discord) | ✅ |
| [Last.fm](/configuration/clients/lastfm) | ✅ |
| [Libre.fm](/configuration/clients/librefm) | ✅ |
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
| [Koito](/configuration/clients/koito) | ❌ |
| [Maloja](/configuration/clients/maloja) | ❌ |
| [Rocksky](/configuration/clients/rocksky) | ❌ |
| [Teal.fm](/configuration/clients/tealfm) | ❌ |
## 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.
After it stops _automatically_ retrying the scrobble still exists and can be retried from the dashboard. Dead scrobbles persist between restart so you will never lose a scrobble that hasn't been successfully submitted yet.
### Now Playing
Multi-scrobbler can report the currently **playing** tracks it is monitoring to some Scrobble Clients via their individual **Now Playing** functionality.
Now Playing is **default enabled** for all Clients that support it.
The behavior multi-scrobbler uses for determining Now Playing reporting:
* Now Playing can be explicitly enabled or disabled globally using ENV `NOW_PLAYING=true` or `NOW_PLAYING=false`
* This only affects Clients that don't have behavior set via File/AIO (below)
* Now Playing will be only be reported to the same Clients a Source is configured to scrobble to
* A Source's Player status must be **Playing**, IE **active**, for reporting to occur
* This status can be verified via Player UI on the Dashboard
* If multiple Sources having active Players then the scrobble Client will default to reporting the track based on Source **configuration** name, alphabetically
* This is the `name` property set in [File or AIO source configs](/configuration?configType=file#configuration-types)
Clients can customize the Now Playing behavior individually using [File or AIO source configs:](/configuration?configType=file#configuration-types)
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"name": "myLastFmClient",
"configureAs": "client",
"data": {
// ...
},
"options": {
// disable or enable
//"nowPlaying": true
//
// OR define a list of Source *config* names that should be allowed to report Now Playing
// order of the list determines priority for reporting Now Playing
//"nowPlaying": ["mySpotify1","myJellyfin2"]
}
},
]
```
@@ -0,0 +1,7 @@
{
"label": "Discord",
"link": {
"type": "doc",
"id": "configuration/clients/discord/discord"
}
}
@@ -0,0 +1,566 @@
---
title: Discord (App)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import TcpWarning from "@site/src/components/snippets/_discord-port-warn.mdx";
import JsonConfig from '!!raw-loader!@site/../config/discord-local.json.example';
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
<img src={require('/img/discord_presence_art.png').default} height="127"/>
This integration uses an **existing Discord application** to set your Rich Presence.
:::tip
Check the [main Discord docs page](/configuration/clients/discord) for a comparison between **App** and **Headless**.
:::
## Required Setup
### Discord Application
You must create a [Discord App](https://docs.discord.com/developers/quick-start/overview-of-apps) in order to use this integration. Creating an App is free and simple.
Go to the [Discord Developer Portal](https://discord.com/developers/applications)
* Click on "New Application" and give it a name
* Copy the **Application Id** (Client Id) shown after creation
* Add this Id to your Multi-Scrobbler config
* Env Config => `DISCORD_APPLICATION_ID=12345678`
* File Config => `"applicationId": "12345678"`
### Discord Connection
This integration uses an existing, running Discord application to communicate with Discord and set Rich Presence. Multi-scrobbler must be able to communicate with the Discord application, either by being on the same machine or over a network.
<Tabs groupId="discordType" queryString>
<TabItem value="container" label="Containerized Discord">
[Kasm Workspaces](https://kasm.com) provides a self-hostable [Discord container image](https://hub.docker.com/r/kasmweb/discord). This image provides a desktop-like experience, through browser-based VNC, to use the official Discord application.
<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#discord-connection) application on a specific machine.
**Advantages**
* Setup is much simpler
* Discord runs in the same stack as multi-scrobbler
* **Configuration is copy-paste.** Skip all of the [Discord Connection](#discord-connection) instructions.
* Get a [Headless](/configuration/clients/discord/discord-headless)-like experience without needing to break Discord TOS
**Disadvantages**
* Image only available for x86 arch
* Image and container are large (1GB+)
* Discord does not notify *other* clients of notifications in a channel if *any* client is viewing that channel.
* To avoid this, make sure your container discord instance is not viewing any server/channel/dm.
* Sometimes, Discord does not deliver "notifications you missed" to mobile because this depends on *all* clients being offline.
<h2>Setup</h2>
Setup is basically the same as using the instructions for [Discord on Linux](./?osType=linux&discordType=native#find-ipc-connection) with [MS on Same Host (Docker)](./?osType=linux&discordType=native#pass-ipc-to-multi-scrobbler).
<h3>1. Add Discord Service to Multi-Scrobbler Docker Compose Stack</h3>
<details>
<summary>Detailed Explanation</summary>
* Add the `kasmweb/discord` service to your compose stack
* Mount the directory containing Discord's unix socket to a named volume
* Configure MS to use Discord's unix socket
* Mount the unix socket volume into MS's container
* Add ENV `DISCORD_IPC_LOCATIONS` to point to the mounted volume
</details>
Use this example `docker-compose.yaml` to modify your existing multi-scrobbler `docker-compose.yaml`:
<details>
<summary>Example</summary>
```yaml title="docker-compose.yaml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler:latest
environment:
# ...
- DISCORD_APPLICATION_ID=12345678
# add below line
// highlight-start
- DISCORD_IPC_LOCATIONS=/run/discord-ipc-0
// highlight-end
volumes:
# ...
# add below line
// highlight-start
- kasm_run:/run
// highlight-end
# ...
# .. add discord service below
// highlight-start
kasmcord:
container_name: kasmcord
image: kasmweb/discord:1.14.0
ports:
# VNC port to use in web browser
- 6901:6901
shm_size: 512m
environment:
# set a password for VNC access
VNC_PW: hunter2
XDG_RUNTIME_DIR: /run/user/1000
volumes:
- kasm_run:/run/user/1000
- kasm_config:/home/kasm-user/.config
user: "0"
entrypoint: sh -c "chmod 700 /run/user/1000 && chown -R kasm-user:kasm-user /run/user/1000 && chown -R kasm-user:kasm-user /home/kasm-user/.config && su kasm-user -c '/dockerstartup/kasm_default_profile.sh /dockerstartup/vnc_startup.sh /dockerstartup/kasm_startup.sh'"
volumes:
kasm_run:
name: kasm_run
kasm_config:
name: kasm_config
// highlight-end
```
</details>
Then re-deploy your stack.
<h3>2. Login to Discord</h3>
Navigate in your browser to the IP/host Multi-Scrobbler/Discord are running on. Use the `port` set in the `kasmcord` service:
```
# basic auth login
# user: kasm_user
# pass is what was set in env VNC_PW
https://192.168.MY.HOST:6901
```
Login to Discord normally. After login is complete MS should connect automatically when a Now Playing event happens. If it does not or reports authentication failure then restart MS (but not Discord).
If MS works then you are done and can move on to [Optional Setup](#optional-setup).
</TabItem>
<TabItem value="native" label="Native Discord">
Use this method if you want to connect MS to Discord running on a physical desktop.
<DetailsAdmo type="tip" summary="Local MS Installation Requires No Config">
If MS was [installed locally](/installation#local-installation) (not Docker) on the same machine as the Discord application then MS should [automatically detect the correct method](#find-ipc-connection) and [use it](#pass-ipc-to-multi-scrobbler) **without** any configuration from you. This will work for **any OS**.
If MS cannot determine this automatically, follow the steps for your OS below.
</DetailsAdmo>
Follow the sections below to configure MS for your specific environment.
#### Find IPC Connection
Discord uses OS-specific methods to communicate with other applications on the same host, called [IPC](https://en.wikipedia.org/wiki/Inter-process_communication). You may need to determine some details about the IPC method in order for MS to communicate with it correctly.
<Tabs groupId="osType" queryString>
<TabItem value="linux" label="Discord on Linux">
On Linux, Discord uses a [Unix Socket](https://en.wikipedia.org/wiki/Unix_domain_socket) for IPC.
With Discord running, run this command:
```
$ sudo netstat -apeen | grep discord
unix 2 [ ACC ] STREAM LISTENING 11026149 1641787/Discord /home/foxx/.var/app/com.discordapp.Discord/cache/scoped_dirnnxNIn/SingletonSocket
unix 2 [ ACC ] STREAM LISTENING 11036791 1642030/exe /run/user/1000/discord-ipc-0
```
We see that the path we need is:
```
/run/user/1000/discord-ipc-0
```
</TabItem>
<TabItem value="macos" label="Discord on MacOS">
On MacOS, Discord uses a [Unix Socket](https://en.wikipedia.org/wiki/Unix_domain_socket) for IPC.
With Discord running, run this command:
```
$ sudo netstat -apeen | grep discord
unix 2 [ ACC ] STREAM LISTENING 11026149 1641787/Discord /home/foxx/.var/app/com.discordapp.Discord/cache/scoped_dirnnxNIn/SingletonSocket
unix 2 [ ACC ] STREAM LISTENING 11036791 1642030/exe /run/user/1000/discord-ipc-0
```
We see that the path we need is:
```
/run/user/1000/discord-ipc-0
```
</TabItem>
<TabItem value="windows" label="Discord on Windows">
Windows uses [**Named Pipes**](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) for IPC. These aren't easily iteratable without external software, like [Pipetap](https://github.com/sensepost/pipetap).
The syntax for a named pipe is like this:
```
\\.\pipe\app_named_pipe_foo
```
and Discord's names follow this pattern:
```
\\.\pipe\discord-ipc-0
```
Where `0` increments for each different instance of Discord (official, canary, nightly, etc...).
The default Discord pipe name is: `discord-ipc-0`
</TabItem>
</Tabs>
#### Pass IPC to Multi-Scrobbler
After [finding your IPC connection path](#find-ipc-connection) you will need to make it accessible to Multi-Scrobbler.
<Tabs groupId="osType" queryString>
<TabItem value="linux" label="Discord on Linux">
<Tabs groupId="msConnection">
<TabItem value="remote" label="MS on Different Host">
<TcpWarning/>
Use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from Discord's [unix socket](#find-ipc-connection) to a listening TCP port on the same host. Likely, you will want to setup a service to start this command on login.
```shell
$ socat -v TCP-LISTEN:6655,reuseaddr,fork UNIX-CONNECT:/run/user/1000/discord-ipc-0
```
In your MS docker compose file add the `host:port` of the host running Discord/`socat` to `DISCORD_IPC_LOCATIONS` `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=192.168.DISCORD_HOST.IP:6655
```
</TabItem>
<TabItem value="local-docker" label="MS on Same Host (Docker)">
In your MS docker compose file, pass the path of Discord's [unix socket](#find-ipc-connection) as a volume and add this path to `DISCORD_IPC_LOCATIONS` in `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=/run/discord-ipc-0
volumes:
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
```
</TabItem>
<TabItem value="local" label="MS on Same Host (Non-Docker)">
No configuration is required. If MS does not automatically work for you then try the **MS on Same Host (Docker)** method using the `DISCORD_IPC_LOCATIONS` environment variable.
</TabItem>
</Tabs>
</TabItem>
<TabItem value="macos" label="Discord on MacOS">
<Tabs groupId="msConnection">
<TabItem value="remote" label="MS on Different Host">
<TcpWarning/>
Use one of the methods below to setup `socat` on your macOS machine.
<details>
<summary>`socat` installed natively</summary>
Install [`socat`](https://formulae.brew.sh/formula/socat) using homebrew to use it as a native command on your host.
Then, use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from Discord's [unix socket](#find-ipc-connection) to a listening TCP port on the same host. Likely, you will want to setup a service to start this command on login.
```shell
$ socat -v TCP-LISTEN:6655,reuseaddr,fork UNIX-CONNECT:/run/user/1000/discord-ipc-0
```
</details>
<details>
<summary>`socat` in Docker</summary>
Use the [`socat` container](https://hub.docker.com/r/alpine/socat) in combination with the Discord [unix socket](#find-ipc-connection) found earlier:
```yaml title="docker-compose.yaml"
services:
socat:
image: alpine/socat
ports:
- "6655:6655"
volumes:
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
command:
- '-v'
- 'TCP-LISTEN:6655,reuseaddr,fork'
- 'UNIX-CONNECT:/run/discord-ipc-0'
```
</details>
In your MS docker compose file add the `host:port` of the host running Discord/`socat` to `DISCORD_IPC_LOCATIONS` `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=192.168.DISCORD_HOST.IP:6655
```
</TabItem>
<TabItem value="local-docker" label="MS on Same Host (Docker)">
In your MS docker compose file, pass the path of Discord's [unix socket](#find-ipc-connection) as a volume and add this path to `DISCORD_IPC_LOCATIONS` in `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=/discord-ipc-0
volumes:
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
```
</TabItem>
<TabItem value="local" label="MS on Same Host (Non-Docker)">
No configuration is required. If MS does not automatically work for you then try the **MS on Same Host (Docker)** method using the `DISCORD_IPC_LOCATIONS` environment variable.
</TabItem>
</Tabs>
</TabItem>
<TabItem value="windows" label="Discord on Windows">
<Tabs groupId="msConnection">
<TabItem value="remote" label="MS in Docker">
:::note
While it is possible to use [named pipes directly in Docker](https://forums.docker.com/t/volume-mount-path-error/143626/3) this has not been tested and [may only work for windows containers](https://github.com/docker/for-win/issues/6813#issuecomment-632859857). If you are adventurous, try this out and if you can get it working please [report your findings in an issue.](https://github.com/FoxxMD/multi-scrobbler/issues)
:::
:::warning
This method is **experimental** and may be buggy and prone to crashes. If you are a Windows or Rust developer please consider helping improve the [pipe proxy implementation](https://github.com/FoxxMD/named-pipe-to-tcp-proxy).
:::
<TcpWarning/>
Use [**pipeProxy**](https://github.com/FoxxMD/named-pipe-to-tcp-proxy) to connect to the Discord named pipe and expose it on a TCP port.
Download the latest [`pipeProxy.exe`](https://github.com/FoxxMD/named-pipe-to-tcp-proxy/releases) from the releases page.
The host/ip should be the IP of the machine running discord.
Run pipeProxy from the windows command line:
```
pipeProxy.exe --pipe "\\.\pipe\discord-ipc-0" --tcp "192.168.0.100:6655"
```
In your MS docker compose file add the `host:port` of the host running `pipeProxy` to `DISCORD_IPC_LOCATIONS` `environment:`
```yaml title="docker-compose.yaml"
environment:
# ...
DISCORD_IPC_LOCATIONS=192.168.0.100:6655
```
</TabItem>
<TabItem value="local" label="MS on Same Host (Non-Docker)">
No configuration is required. If MS does not automatically work for you then try the **MS in Docker** method using the `DISCORD_IPC_LOCATIONS` environment variable.
</TabItem>
</Tabs>
</TabItem>
</Tabs>
</TabItem>
</Tabs>
## Optional Setup
:::note
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.
:::
### Artwork
Multi-scrobbler can display Album Art in your Discord status. The image that is used differs based on if the image URL is publically accessible.
The order in which multi-scrobbler determines what image to use is the same as the order of the tabs shown below.
#### Artwork URL Types
<Tabs groupId="artworkUrl" queryString>
<TabItem label="External URLs" value="external">
MS can use external URLs for artwork.
<img src={require('/img/discord_presence_art.png').default} height="127"/>
External URLs that are not from known music services are **disabled by default.** This is to preserve *your* privacy because Discord does not host images, it only links to them and the URL is visible to other users.
<details>
<summary>Why Should I Care?</summary>
There are some scenarios where you may not want your artwork URLs to be visible, for example:
* A Source may be accessible only on your LAN (Jellyfin at `http://192.168.0.101`)
* A Source may be internet-facing but you do not want to expose this information to discord/other users
* A Source may be internet-facing but requires authentication to view the image
</details>
MS will use your artwork URLs only if:
* artwork url is from a known music service/[Cover Art Archive](./?artworkUrl=caa#artwork-url-types) or
* artwork url starts with `https` and
* `artwork` (`DISCORD_ARTWORK`) is
* `true` => always uses the url
* a list of custom domains keywords an external artwork url should contain EX
* ENV => `DISCORD_ARTWORK=mycdn,jellyfin`
* File Config => `"artwork": ["mycdn","jellyfin"]`
<details>
<summary>Examples</summary>
None of the below URLs will **ever** be used because they are not `https`
```
http://foobar.com/cool.jpg <-- NOT allowed, not http
http://192.168.0.112/fun.png <-- NOT allowed, not http
```
When `DISCORD_ARTWORK` is not set:
```
https://archive.org/download/dir/myimage.jpg <-- allowed, is https and known service
https://spotify.com/assets/cover.png?size=1000 <-- allowed, is https and known service
https://example.com/cool.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
https://anything.com/neat.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
```
Setting `DISCORD_ARTWORK=true`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- allowed, is https
https://anything.com/neat.jpg <-- allowed, is https
```
Setting `DISCORD_ARTWORK=mycdn,jellyfin`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- NOT allowed, is https but does not contain "mycdn" or "jellyfin"
https://jellyfin.mydomain.com/neat.jpg <-- allowed, is https and contains "jellyfin"
```
</details>
</TabItem>
<TabItem label="Musicbrainz/Cover Art Achive" value="caa">
MS can try to use [Cover Art Archive](http://coverartarchive.org/) to get album art. The URL it retrieves is public which means it acts like an External URL that is always available for you.
<img src={require('/img/discord_presence_art.png').default} height="127"/>
MS will try to use Cover Art Archive if:
* The scrobble data does not contain an album art url (no art in dashboard) or the existing url has failed an [External URL](./?artworkUrl=external#artwork-url-types) condition
* and **the scrobble data contains a [Musicbrainz](/configuration/transforms/musicbrainz) Release (Album) MBID**
Basically, if MS would normally use the [MS Default](./?artworkUrl=ms-default#artwork-url-types) art, it will first try to use Cover Art Archive if the scrobble has a Release MBID.
Some Source, like [Jellyfin](/configuration/sources/jellyfin) and [Plex](/configuration/sources/plex), can automatically provide this ID if the music is "matched" in your library.
For all others, you can [**configure MS to use the Musicbrainz Stage**](/configuration/transforms/musicbrainz#use-metadata-for-discord-album-art) for Discord to try to get this MBID.
</TabItem>
<TabItem label="MS Default" value="ms-default">
When these conditions are true:
* Cannot use an [External URL](./?artworkUrl=external#artwork-url-types) and
* Cannot get album art from [Cover Art Archive](./?artworkUrl=caa#artwork-url-types) and
* Play does not contain artwork information (no artwork shown in dashboard)
The status artwork will use a default URL instead of your artwork URL.
Without any other configuration, the default URL is an icon from the [multi-scrobbler repository](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/default-artwork.png):
<img src={require('/img/discord_presence_ms_default.png').default} height="127"/>
This default image can be customized:
* ENV Config => `DISCORD_ARTWORK_DEFAULT_URL=https://cooldomain.com/art.png`
* File Config => `"artworkDefaultUrl": "https://cooldomain.com/art.png"`
Remember, this URL need to be accessible on the public internet.
</TabItem>
</Tabs>
## Configuration
<Config config="DiscordClientConfig" fileContent={JsonConfig} client name="discord">
| Environmental Variable | Required? | Default | Description |
| :---------------------------- | --------- | ------- | :--------------------------------------------------------------------------------------------------------------- |
| `DISCORD_APPLICATION_ID` | Yes | | Application ID |
| `DISCORD_IPC_LOCATIONS` | No | | A commera-separated list of `host:port` network locations or file paths to unix socket for Discord |
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
</Config>
@@ -0,0 +1,334 @@
---
title: Discord (Headless)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import JsonConfig from '!!raw-loader!@site/../config/discord.json.example';
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
<img src={require('/img/discord_presence_art.png').default} height="127"/>
This integration is **headless**, it does not depend on any specific Discord application to be running.
:::tip
Check the [main Discord docs page](/configuration/clients/discord) for a comparison between **App** and **Headless**.
:::
<DetailsAdmo type="note" summary="How MS Uses Discord API">
Multi-scrobbler does its upmost to implement, to-spec, [Gateway API communication](https://docs.discord.com/developers/events/gateway) and respect usage. It requires no [permissions](https://docs.discord.com/developers/events/gateway#gateway-intents) and reads no data other than monitoring what other user sessions are active.
Aside from the on-paper violation of using a user token programmatically (on your behalf), there is no misuse of the API to achieve rich presence.
Additionally, MS uses the state of non-MS sessions to conservatively update presence. [By default it will not conflict](#configure-when-presence-is-used) with any other official presence activities and is only used when you are actually active on a device.
</DetailsAdmo>
<DetailsAdmo type="note" summary="REQUIRED Interaction after Starting Multi-Scrobbler">
Every time multi-scrobbler is started a discord session update needs to be triggered so that [MS can capture the signals it uses to determine if presence can be updated.](#configure-when-presence-is-used) This only needs to be *once*, after starting multi-scrobbler. But it does need to be *every time* multi-scrobbler is (re)started. Without these signals MS will not update presence.
This session update can happen automatically after *some* time so you may not need to do anything. If you want to force the update then do one of the following:
* Open discord on a device it was not recently active on
* Exit discord on an active device
* Change your status (online, idle, etc...)
* Set a custom status
* Cause a presence update with a different app (listening to..., playing..., etc.)
</DetailsAdmo>
## Required Setup
### User Token
You must provide a **Discord User Token** for your account in order to authenticate with Discord.
<DetailsAdmo type="warning" summary="User Token usage violates Discord TOS">
This functionality requires you to use your own **Discord User Token** in a way that is **against Discord's policies/terms of service.** If you use this scrobbler you do so **at your risk**.
There is no precedent for Discord punishing users who use their own token for non-abuse/spam reasons but there is also no guarantee this lack of enforcement will continue.
Refer to the **How MS Uses Discord API** section above to gauge whether this risk is acceptable to you.
</DetailsAdmo>
<DetailsAdmo type="warning" summary="User Token is a Sensitive Credential">
Treat your User Token like a login credential. Anyone who has your token can access Discord as you.
When finding instructions for obtaining this token only run commands, or follow steps, that you can understand and trust. Do not run commands you do not understand. Never give your token to someone else.
</DetailsAdmo>
No instructions will be provided for obtaining a User Token because it is against Discord's policies to use it. Instead, you can search online for "how to obtain discord user token".
## Optional Setup
### Configure When Presence Is Used
MS uses several signals from your "real" Discord sessions to determine if it should update presence. The default configuration is conservative to ensure that MS does not conflict with other apps using presence. Additionally, it only updates if a "real" user device is active on discord.
:::tip[TLDR]
Without any additional configuration, MS will only update presence if:
* you have discord open on a real device
* your status is either: online, idle, or dnd (not invisible)
* no other apps are broadcasting listening activities (listening to spotify, etc...)
:::
#### Online Status
Configure if MS is allowed to update presence based on your online status. If this setting is not defined MS will only update if your status is **online**, **idle**, or **dnd**.
Allowed Values: `online` `idle` `dnd` `invisible`
<details>
<summary>Example</summary>
Only allow presence updating when all of your "real" sessions are either online or idle.
In File/AIO:
```json
[
{
"name": "MS",
"enable": true,
"data": {
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
"applicationId": "8190211179716453570",
"statusOverrideAllow": ["online", "idle"]
}
}
]
```
In ENV:
```
DISCORD_STATUS_OVERRIDE_ALLOW=online,idle
```
</details>
#### Other Listening Activities
Without any additional configuration, MS will not set Rich Presence if **other** apps are broadcasting **listening** activities EX "Listening to Spotify".
<details>
<summary>What about other activity types?</summary>
Discord uses an undocumented activity type priority to determine what activity is shown "first" for your profile. The Listening activity type has the *lowest priority*.
This means that if you start any other type of activity (streaming, watching, competing, etc...) it will take precedence over MS's listening activity.
Your listening activity will still be visible by clicking into your full profile.
</details>
Configure the names of **other** listening activities that MS **is allowed** to broadcast at the same time as.
<details>
<summary>Example</summary>
You want to allow MS to update presence even if Spotify is also broadcasting a listening activity.
In File/AIO:
```json
[
{
"name": "MS",
"enable": true,
"data": {
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
"applicationId": "8190211179716453570",
"listeningActivityAllow": ["spotify"]
}
}
]
```
In ENV:
```
DISCORD_LISTENING_ACTIVITY_ALLOW=spotify
```
</details>
### Discord Application
Creating a [Discord App](https://docs.discord.com/developers/quick-start/overview-of-apps) enables you to use custom album cover art and secondary status images for more links. Creating an App is free and simple, it is recommended you do this to get the most functionality from Rich Presence.
<details>
<summary>Create An Application</summary>
Go to the [Discord Developer Portal](https://discord.com/developers/applications)
* Click on "New Application" and give it a name
* Copy the **Application Id** (Client Id) shown after creation
* Add this Id to your Multi-Scrobbler config
* Env Config => `DISCORD_APPLICATION_ID=12345678`
* File Config => `"applicationId": "12345678"`
</details>
### Artwork
Multi-scrobbler can display Album Art in your Discord status. The image that is used differs based on if the image URL is publically accessible.
:::note
If no [**Application Id**](#discord-application) is configured the status art will **always** be the [Discord Default](./?artworkUrl=discord-default#artwork-url-types).
:::
The order in which multi-scrobbler determines what image to use is the same as the order of the tabs shown below.
#### Artwork URL Types
<Tabs groupId="artworkUrl" queryString>
<TabItem label="External URLs" value="external">
When [**Application Id**](#discord-application) **is** configured MS can use external URLs for artwork.
<img src={require('/img/discord_presence_art.png').default} height="127"/>
External URLs that are not from known music services are **disabled by default.** This is to preserve *your* privacy because Discord does not host images, it only links to them and the URL is visible to other users.
<details>
<summary>Why Should I Care?</summary>
There are some scenarios where you may not want your artwork URLs to be visible, for example:
* A Source may be accessible only on your LAN (Jellyfin at `http://192.168.0.101`)
* A Source may be internet-facing but you do not want to expose this information to discord/other users
* A Source may be internet-facing but requires authentication to view the image
</details>
MS will use your artwork URLs only if:
* artwork url is from a known music service/[Cover Art Archive](./?artworkUrl=caa#artwork-url-types) or
* artwork url starts with `https` and
* `artwork` (`DISCORD_ARTWORK`) is
* `true` => always uses the url
* a list of custom domains keywords an external artwork url should contain EX
* ENV => `DISCORD_ARTWORK=mycdn,jellyfin`
* File Config => `"artwork": ["mycdn","jellyfin"]`
<details>
<summary>Examples</summary>
None of the below URLs will **ever** be used because they are not `https`
```
http://foobar.com/cool.jpg <-- NOT allowed, not http
http://192.168.0.112/fun.png <-- NOT allowed, not http
```
When `DISCORD_ARTWORK` is not set:
```
https://archive.org/download/dir/myimage.jpg <-- allowed, is https and known service
https://spotify.com/assets/cover.png?size=1000 <-- allowed, is https and known service
https://example.com/cool.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
https://anything.com/neat.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
```
Setting `DISCORD_ARTWORK=true`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- allowed, is https
https://anything.com/neat.jpg <-- allowed, is https
```
Setting `DISCORD_ARTWORK=mycdn,jellyfin`
```
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
http://foobar.com/cool.jpg <-- NOT allowed, not http
https://example.com/cool.jpg <-- NOT allowed, is https but does not contain "mycdn" or "jellyfin"
https://jellyfin.mydomain.com/neat.jpg <-- allowed, is https and contains "jellyfin"
```
</details>
</TabItem>
<TabItem label="Musicbrainz/Cover Art Achive" value="caa">
When [**Application Id**](#discord-application) **is** configured MS can try to use [Cover Art Archive](http://coverartarchive.org/) to get album art. The URL it retrieves is public which means it acts like an External URL that is always available for you.
<img src={require('/img/discord_presence_art.png').default} height="127"/>
MS will try to use Cover Art Archive if:
* The scrobble data does not contain an album art url (no art in dashboard) or the existing url has failed an [External URL](./?artworkUrl=external#artwork-url-types) condition
* and **the scrobble data contains a [Musicbrainz](/configuration/transforms/musicbrainz) Release (Album) MBID**
Basically, if MS would normally use the [MS Default](./?artworkUrl=ms-default#artwork-url-types) art, it will first try to use Cover Art Archive if the scrobble has a Release MBID.
Some Source, like [Jellyfin](/configuration/sources/jellyfin) and [Plex](/configuration/sources/plex), can automatically provide this ID if the music is "matched" in your library.
For all others, you can [**configure MS to use the Musicbrainz Stage**](/configuration/transforms/musicbrainz#use-metadata-for-discord-album-art) for Discord to try to get this MBID.
</TabItem>
<TabItem label="MS Default" value="ms-default">
When [**Application Id**](#discord-application) **is** configured and these conditions are true:
* Cannot use an [External URL](./?artworkUrl=external#artwork-url-types) and
* Cannot get album art from [Cover Art Archive](./?artworkUrl=caa#artwork-url-types) and
* Play does not contain artwork information (no artwork shown in dashboard)
The status artwork will use a default URL instead of your artwork URL.
Without any other configuration, the default URL is an icon from the [multi-scrobbler repository](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/default-artwork.png):
<img src={require('/img/discord_presence_ms_default.png').default} height="127"/>
This default image can be customized:
* ENV Config => `DISCORD_ARTWORK_DEFAULT_URL=https://cooldomain.com/art.png`
* File Config => `"artworkDefaultUrl": "https://cooldomain.com/art.png"`
Remember, this URL need to be accessible on the public internet.
</TabItem>
<TabItem label="Discord Default" value="discord-default">
When [**Application Id**](#discord-application) is **not** configured Discord will show its own image. No custom artwork can be used in this configuration.
This is also the image Discord will display if the artwork URL cannot be fetched.
<img src={require('/img/discord_presence_discord_default.png').default} height="127"/>
</TabItem>
</Tabs>
## Configuration
<Config config="DiscordClientConfig" fileContent={JsonConfig} client name="discord">
| Environmental Variable | Required? | Default | Description |
| :--------------------------------- | --------- | ----------------- | :---------------------------------------------------------------------------------------------------------------- |
| `DISCORD_TOKEN` | Yes | | User Token acquired from an active Discord sessions |
| `DISCORD_APPLICATION_ID` | No | | Application ID used to display album art |
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
| `DISCORD_STATUS_OVERRIDE_ALLOW` | No | `online,idle,dnd` | A comma-separated list of statuses are allowed to have presence. |
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | No | | A commera-seperated list of listening activity names MS **is allowed** to broadcast presence at the same time as. |
</Config>
@@ -0,0 +1,38 @@
---
sidebar_position: 2
title: Discord
description: Discord Client Overview
toc_max_heading_level: 4
---
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
<img src={require('/img/discord_presence_art.png').default} height="127"/>
There are two types of Discord integrations to choose from:
## App
The **App** integration uses an **existing** Discord application on a desktop computer to set Rich Presence.
* Uses "permitted" access to Discord, no TOS issues
* More restricted customization compared to Headless
* Only active when your existing Discord application is open
* For most scenarios it requires additional, OS-specific steps to setup correctly
* May require an additional program on your machine to relay the Discord connection
* This is potentially insecure, or less secure, then Headless
* **Can use [discord in a container](/configuration/clients/discord/discord-app#discord-connection) as an easy alternative**
* This is a Headless-like experience
[**App Docs**](/configuration/clients/discord/discord-app)
## Headless
The **Headless** integration uses Discord cloud Gateway/API, directly, to set Rich Presence.
* Does not require an existing Discord application (uses Discord cloud API)
* Using this **Violates Discord TOS** because it requires a user token
* More customization than App integration
* Active if **any** Discord application is online
[**Headless Docs**](/configuration/clients/discord/discord-headless)
@@ -0,0 +1,60 @@
---
title: Koito
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/koito.json.example';
:::tip[Other Uses]
To monitor and re-scrobble activity from a Koito account create a [Koito (Source)](/configuration/sources/koito-source)
:::
Setup a [Koito server](https://koito.io/guides/installation/) if you have not already done this.
* [Create (or get) an API Key](https://koito.io/guides/scrobbler/)
* From your Koito server dashboard navigate to **Settings** (bottom-left icon) => **API Keys**
* Create a new Key (or use an existing) and Copy the key using the copy icon to the right of the key
* EX `pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL`
* Take note of your Koito username (used below as `KOITO_USER`)
* Determine your Koito URL
* This is the URL you use to access your Koito dashboard OR whatever URL is accessible from your multi-scrobbler instance.
* EX dashboard at `http://192.168.0.100:4110` => `KOITO_URL` is set to `http://192.168.0.100:4110`
:::note[Koito Base URL]
Using the URL path `/apis/listenbrainz` [Koito docs describe](https://koito.io/guides/scrobbler/) **is not required.** The Koito URL used for multi-scrobbler should only be the **base** URL. Only include a URL path if your Koito **base** url is not at `host:port`. See the table below for examples.
<details>
<summary>Base URL Examples</summary>
| `KOITO_URL` | MS Detected Base URL | Example Scrobble Api URL |
| :-------------------------------------------- | :------------------------------ | ---------------------------------------------------- |
| `http://192.168.0.100:4110` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
| `https://koito.mydomain.com` | `https://koito.mydomain.com` | `https://koito.mydomain.com/apis/listenbrainz/1` |
| `http://192.168.0.100:4110/apis/listenbrainz` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
| `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito/apis/listenbrainz/1` |
</details>
:::
:::tip
Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito config set [`KOITO_ALLOWED_HOSTS`](https://koito.io/reference/configuration/#koito_allowed_hosts) to the IP the multi-scrobbler dashboard is accessible from.
:::
## Configuration
<Config config="KoitoClientConfig" fileContent={JsonConfig} client name="koito">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | --------------------------------- |
| `KOITO_TOKEN` | Yes | | API Key from your Koito Account |
| `KOITO_USER` | Yes | | Your Koito username |
| `KOITO_URL` | No | | The base URL for the Koito server |
</Config>
@@ -0,0 +1,58 @@
---
title: Last.fm
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/lastfm.json.example';
:::tip[Other Users]
* To monitor and re-scrobble activity from your Last.fm/Libre.fm account create a [Last.fm (Source)](/configuration/sources/lastfm-source)
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
:::
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](#callbackredirect-url) section.
**Take note of your API Key and Secret after account creation.**
### Callback/Redirect URL
The Redirect (Callback) URL is used to redirect your **browser** to multi-scrobbler after authentication has completed.
So, the *base of the URL* you use should be the *same* as the URL you would use to navigate to the multi-scrobbler dashboard.
<details>
<summary>Example</summary>
If you can visit the multi-scrobbler dashboard by navigating to http://192.168.0.100:9078 in your web browser...
then the ENV `LASTFM_REDIRECT_URI`, or `redirectUri` for last.fm/libre.fm json config, should be
```
http://192.168.0.100:9078/lastfm/callback
```
</details>
The Redirect URL can be explicitly specified in the configuration below, or by using the global [Base URL](/configuration#base-url) setting.
## Configuration
<Config config="LastfmClientConfig" fileContent={JsonConfig} client name="lastfm">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
| `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
</Config>
@@ -0,0 +1,65 @@
---
title: Libre.fm
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/librefm.json.example';
:::tip[Other Users]
* To monitor and re-scrobble activity from your Libre.fm account create a [Libre.fm (Source)](/configuration/sources/librefm-source)
:::
To use the official [Libre.fm](https://libre.fm/) instance you will need an account. Or, you can run your own Libre.fm instance [using the legacy source code](https://github.com/foocorp/gnu-fm).
No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is needed to use the Libre.fm API. Instead, use **any values** for API Key/Secret in the configuration. Authentication is done by allowing access from your account on redirect.
### Callback/Redirect URL
The Redirect (Callback) URL is used to redirect your **browser** to multi-scrobbler after authentication has completed.
So, the *base of the URL* you use should be the *same* as the URL you would use to navigate to the multi-scrobbler dashboard.
<details>
<summary>Example</summary>
If you can visit the multi-scrobbler dashboard by navigating to http://192.168.0.100:9078 in your web browser...
then the ENV `LIBREFM_REDIRECT_URI`, or `redirectUri` for libre.fm json config, should be
```
http://192.168.0.100:9078/librefm/callback
```
</details>
The Redirect URL can be explicitly specified in the configuration below, or by using the global [Base URL](/configuration#base-url) setting.
### Custom Libre.fm instance
If you are **not** using the official Libre.fm instance then specify your custom instance using ENV `LIBREFM_URLBASE` or `urlBase` property in the `data` section in File/AIO config.
The value should be `protocol://libre.tld/path/prefix` EX:
```
https://my.cool.libre.com/2.0/
```
## Configuration
<Config config="LibrefmClientConfig" fileContent={JsonConfig} client name="librefm">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
| `LIBREFM_API_KEY` | No | `anything` | Optional API Key |
| `LIBREFM_SECRET` | No | `anything` | Optional shared secret |
| `LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
| `LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
| `LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
</Config>
@@ -0,0 +1,31 @@
---
title: Listenbrainz
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/listenbrainz.json.example';
:::tip[Other Users]
* To monitor and re-scrobble activity from a Listenbrainz account create a [Listenbrainz (Source)](/configuration/sources/listenbrainz-source)
* To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
:::
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
## Configuration
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
| `LZ_TOKEN` | Yes | | User token from your LZ profile |
| `LZ_USER` | Yes | | Your LZ username |
| `LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
</Config>
@@ -0,0 +1,45 @@
---
title: Maloja
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/maloja.json.example';
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
<details>
<summary>Maloja Setup Intructions</summary>
Using Maloja's example `docker-compose.yml`:
```yaml reference title="~/malojaData/docker-compose.yml"
https://github.com/krateng/maloja/blob/master/example-compose.yml
```
Uncomment `environment` and add `MALOJA_FORCE_PASSWORD=CHANGE_ME` to set an admin password
Start the container:
```shell title="~/malojaData"
docker compose up -d
```
</details>
* Navigate to the Admin Panel (Cog in upper-right corner) -> API Keys (or at http://myMalojaServerIP/admin_apikeys)
* Create a **New Key** and then copy the generated key value
Finally, add the Maloja server URL and API Key to the configuration type you choose to use, below.
## Configuration
<Config config="MalojaClientConfig" fileContent={JsonConfig} client name="maloja">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|-------------------------------|
| `MALOJA_URL` | Yes | | Base URL of your installation |
| `MALOJA_API_KEY` | Yes | | Api Key |
</Config>
@@ -0,0 +1,72 @@
---
title: Rocksky
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
:::tip[Other Uses]
To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)](/configuration/sources/rocksky-source)
:::
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
<details>
<summary>How does that work?</summary>
<Bluesky name="Rocksky" lexicon="app.rocksky"/>
</details>
<DetailsAdmo type="warning" summary="Your Data Is Public">
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
</DetailsAdmo>
<DetailsAdmo type="warning" summary="No Scrobbling Guarantee">
Rocksky **does not guarantee** that every scrobble you send will be persisted. Rocksky's design goal is to store scrobbles with **metdata-complete, 3rd party verifiable track information** so that it can be a metadata-rich discovery platform, rather than an exhaustive scrobble data source-of-truth.
To do this it *asynchronously* matches your scrobble data against Musicbrainz's and/or Spotify's catalogs, *after the scrobble has been submitted.* **If the scrobble does not end up matching then it is not persisted.**
If you need your scrobbles to be 100% captured then you should scrobble to an *additional* [Scrobble Client](/configuration/clients).
</DetailsAdmo>
## Setup
You will need a [Bluesky](https://bsky.app/) account to use Rocksky.
:::note
Usage is adapted from [Rocksky docs](https://docs.rocksky.app/migrating-to-rocksky-scrobble-api-957839m0) on [docs.rocksky.app](https://docs.rocksky.app)
:::
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Bluesky account
* Then, navigate to the [RockSky developer dashboard](https://rocksky.app/apikeys) and obtain an API Key
## Configuration
### Handle
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
* For regular Bluesky account this will be like: `alice.bsky.social`
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
* For non-Bluesky users, you probably already know your handle
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">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
| `ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard |
</Config>
@@ -0,0 +1,78 @@
---
title: Teal.fm
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/tealfm.json.example';
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
:::tip[Other Uses]
To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (Source)](/configuration/sources/tealfm-source)
:::
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
<details>
<summary>How does that work?</summary>
<Bluesky name="teal.fm" lexicon="fm.teal"/>
</details>
<DetailsAdmo type="warning" summary="Your Data Is Public">
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
</DetailsAdmo>
<DetailsAdmo type="note" summary="teal.fm Is Still In Development">
[teal.fm is still in developement](https://notes.teal.fm/3lzlwe6puis2l) and does not have a true production website, yet.
To view your teal.fm scrobble data you can:
* use [teal-relay-production.up.railway.app](https://teal-relay-production.up.railway.app) or [teal-slice.wisp.place](https://tealfm-slice.wisp.place/) for a preview of teal.fm
* use [yearinmusic.teal.fm](https://yearinmusic.teal.fm) or [wrapped.baileytownsend.dev](https://wrapped.baileytownsend.dev/) for wrapped-style stats of your teal.fm scrobbles
* use [atproto.at](https://atproto.at) to view the raw data (convenience URLs are generated in multi-scrobbler, per scrobble, for you to do this)
</DetailsAdmo>
## Setup
#### Bluesky
* Create a [Bluesky](https://bsky.app) account, if you don't already have one
* Navigate to https://bsky.app/settings/app-passwords
* Settings -> Privacy and Security -> App passwords
* Create a new **App Password** and save it somewhere, it will not be shown again
## Configuration
### Identifier
The ATPRoto identifier used with multi-scrobbler should be either:
* A valid [DID](https://atproto.com/specs/did#at-protocol-did-identifier-syntax), starting with `did:plc:...` or `did:web:...`
* Your **full** ATProto handle, including TLD
If using a handle:
* For regular Bluesky account this will be like: `alice.bsky.social`
* For Bluesky accounts that [use their domain as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
* For non-Bluesky-PDS users, you probably already know your handle
If your identifier does not look like a DID and does not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
<Config config="TealClientConfig" fileContent={JsonConfig} client name="tealfm">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| `TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
| `TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password |
</Config>
File diff suppressed because it is too large Load Diff
@@ -1,23 +1,25 @@
---
sidebar_position: 3
title: Kitchen Sink
description: Kitchen Sink Example
---
# Example Config using all Possible Features
# Example Complex Scenario
Scenario:
* You want to scrobble plays for yourself (Foxx), Fred, and Mary
* Each person has their own Maloja server
* Each person has their own [Maloja](/configuration/clients/maloja) server
* Each person has their own Spotify account
* You have your own Airsonic (subsonic) server you to scrobble from
* You have your own Youtube Music account you want to scrobble from
* Mary has her own Last.fm account she also wants to scrobble to
* Fred has his own Spotify application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you 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 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 and wants to scrobble everything he plays
* 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](/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
@@ -124,6 +126,14 @@ Scenario:
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
}
},
{
"type": "endpointlz",
"name": "listenbrainzfoxx",
"clients": ["foxxMaloja"],
"data": {
"token": "myToken"
}
}
],
"clients": [
{
@@ -246,7 +256,6 @@ Scenario:
```json5 title="CONFIG_DIR/ytmusic.json"
[
{
"type": "ytmusic",
"name": "foxxYoutube",
"clients": ["foxxMaloja"],
"data": {
@@ -255,6 +264,17 @@ Scenario:
}
]
```
```json5 title="CONFIG_DIR/endpointlz.json"
[
{
"name": "listenbrainzfoxx",
"clients": ["foxxMaloja"],
"data": {
"token": "myToken",
}
}
]
```
```json5 title="CONFIG_DIR/maloja.json"
[
-81
View File
@@ -1,81 +0,0 @@
---
sidebar_position: 2
title: Plex/Tautulli (Deprecated)
---
:::warning[Deprecated]
Multi-scrobbler >= 0.8.8 supports a Plex Source that [directly uses the API](configuration/configuration.mdx?plexType=api#plex) which removes the need to use Tautulli since it does not require Plex Pass.
The Tautulli source **and** Plex Webhook source are deprecated and will be removed in an upcoming release. Existing users should migrate to the API source as soon as possible.
:::
Tracks played on [Plex](https://plex.tv/) can be scrobbled either by:
* A [Tautulli](https://tautulli.com/) notification agent with a webhook.
* Using Plex [Webhooks](https://support.plex.tv/articles/115002267687-webhooks) (restricted to Plex Pass users)
# Using Tautulli
## Create a new Notification Agent
* Navigate to the **Notification Agents** page in **Settings**
* Click **Add a new notification agent**
* Select **Webhook**
## Configure the Agent
The below sections correspond with the tabs available in the notification agent configuration popup.
### Configuration
* Webhook URL -- `http://localhost:9078/tautulli` (substitute your domain if different than the default)
* Webhook Method -- POST
### Triggers
Select **Watched**
### Conditions
Refer to [Tautulli's documentation](https://github.com/Tautulli/Tautulli-Wiki/wiki/Custom-Notification-Conditions) if you need help here. It may be a good idea to restrict notifications to only one library (if you have a Music library, for instance)
**This app will only scrobble an item if `media_type` is a "track", which is the default for all music.**
### Data
Expand the **Watched** dropdown and add the following code block to the **JSON Data** text field:
```
{
"artist_name": "{artist_name}",
"track_name": "{track_name}",
"track_artist": "{track_artist}",
"album_name": "{album_name}",
"media_type": "{media_type}",
"title": "{title}",
"duration": "{duration_sec}",
"username": "{username}",
"server": "{server_name}",
"version": "{server_version}",
"library": "{library_name}",
"player": "{player}",
"device": "{device}",
"platform": "{platform}",
"action": "{action}",
"machine_id": "{machine_id}",
"session_key": "{session_key}"
}
```
**Click the Save button to finish.**
Your agent is now configured and ready to scrobble.
# Using Plex Webhooks
* Navigate to your **Account/Settings** and find the **Webhooks** page
* Click **Add Webhook**
* URL -- `http://localhost:9078/plex` (substitute your domain if different than the default)
* **Save Changes**
Plex is now configured to scrobble.
@@ -0,0 +1,7 @@
{
"label": "Scrobble Sources",
"link": {
"type": "doc",
"id": "configuration/sources/sources"
}
}
@@ -0,0 +1,59 @@
---
title: Azuracast
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 AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example';
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
:::tip[Scrobbling Threshold]
Azuracast may not correctly report track length or position. If this is the case you should reduce the "listened to" **duration** using [Scrobble Thresholds](/configuration/sources#scrobble-thresholds) so that multi-scrobbler scrobbles correctly.
:::
The Azuracast server should have **Use High-Performance Now Playing Updates** enabled in _Administration -> System Settings_
#### URL
The URL used by MS to connect to Azuracast has the syntax:
```
[ws|wss]://HOST:[PORT]
```
MS will automatically add the path required for websockets, [`/api/live/nowplaying/websocket`](https://www.azuracast.com/docs/developers/now-playing-data/#websockets), to your URL if none is provided. If you use a reverse proxy with a path-based URL or otherwise need a custom path to access the websockets path correctly then explicitly provide it. Examples:
```
URL From Config => MS Uses
'ws://192.168.0.101' => ws://192.168.0.101/api/live/nowplaying/websocket
'ws://azura.mydomain.com' => ws://azura.mydomain.com.com/api/live/nowplaying/websocket
'wss://mydomain.com/custom/azura/ws' => wss://mydomain.com/custom/azura/ws
```
<ShouldScrobble filename="azuracast"/>
Multi-scrobbler will set the default **scrobbling behavior** based on if either of these settings is configured:
* live status (`AZURA_LIVE` or `monitorWhenLive`) or
* listener count (`AZURA_LISTENERS_NUM` or `monitorWhenListeners`)
If either is configured then MS will automatically scrobble based on when these conditions are met.
If neither is configured automatic scrobble defaults to **off** and you should either manually initiate it or explicitly set the default behavior.
## Configuration
<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` |
</Config>
@@ -0,0 +1,201 @@
---
title: Deezer
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 DeezerConfig from '!!raw-loader!@site/../config/deezer.json.example';
import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecated.json.example';
<Tabs groupId="deezerSource" queryString>
<TabItem value="arl" label="Unofficial ARL">
:::warning
This Source uses unofficial methods to access Deezer data that are likely in violation of Deezer's TOS. Deezer may change or remove these methods at any time, breaking functionality. Use this Source at your own risk.
:::
:::info
This Source will work only if you have a **Premium** Deezer account.
:::
### Retrieve ARL
The `arl` is a browser cookie that Deezer uses to store your login information. Multi-scrobbler uses the `arl` value to make authorized requests to Deezer's API where it can retrieve listening history.
**After logging into Deezer**, use one of the methods below to retrieve the `arl` cookie value:
<Tabs groupId="arlRetrival">
<TabItem value="chrome" label="Chrome">
1. On deezer.com, press F12 to open the Developer Tools window.
2. Go to the Applications tab. Press the '>>' More tabs icon if it's hidden.
3. Under the Cookies Filter, click deezer.com, then the text bar name `arl`.
4. Copy all the contents shown in the **Cookie Value** pane.
</TabItem>
<TabItem value="firefox" label="Firefox">
1. On deezer.com, hit Ctrl+Shift+I to open Developer Tools
2. Go to the Storage Tab, then expand Cookies in the sidebar and select deezer.com
3. Find the row with `arl` as the name, then double click the Value column and right click -> copy the value
</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"`
<details>
<summary>deezer.json example</summary>
```json title="deezer.json"
[
{
"name": "MyDeezer",
"data": {
"arl": "UOsRPjT3U5Dhaaup3x...",
},
"options": {
"fuzzyDiscoveryIgnore": "aggressive"
}
}
]
```
</details>
This option comes with some trade-offs: MS will aggressively detect repeated tracks within a window of time that should eliminate all duplicates. However, this will also prevent *intentionally* repeated tracks from being scrobbled. See [this thread](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2970417070) for more information on how this works.
### Family account
Multi-scrobbler can monitor listening history for accounts linked to a [Deezer Family Account](https://www.deezer.com/en/offers/family).
<details>
<summary>Instructions</summary>
Start multi-scrobbler using the [ARL](#retrieve-arl) for the **main account**.
In the logs look for **Linked Accounts** associated with the Deezer Source. It will look something like this:
```
VERBOSE: [App] [Sources] [Deezer - MyDeezer] Linked Accounts:
Name: Joe | ID: 166276334 | Private?: Yes
Name: Mary | ID: 48475231 | Private?: Yes
Name: FoxxMD | ID: 896225281 | Private?: No
Name: Cool Guy | ID: 128522478 | Private?: Yes
```
The Account **ID** you want to monitor should be set:
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
Set in your docker compose `environment` section:
```yaml
- DEEZER_ACCOUNT_ID=896225281
```
</TabItem>
<TabItem value="file" label="File or AIO">
Add an `accountId` property to the `data` section
```json
[
{
"name": "DeezerARL",
"enable": true,
"clients": [],
"data": {
"arl": "UOsRPjT3U5Dhaaup3xQ30D...",
"accountId": "896225281"
}
}
]
```
</TabItem>
</Tabs>
Restart multi-scrobbler to start monitoring that account.
:::warning[Restrictions]
Deezer may not return the correct listening history under these circumstances:
* The ARL used is **not** for the main account
* If you can get an ARL specifically for the linked account (login as the linked account), use that **instead** of family `accountId`
* The ARL account is private
* The linked account to be monitored ( using `accountId`) is private
:::
</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">
:::warning
**This Source is DEPRECATED because Deezer has dropped official API support.** This Source will **not** be removed but no further support or fixes will be given. [See this issue for more discussion.](https://github.com/FoxxMD/multi-scrobbler/issues/175#issuecomment-2296776625)
Users cannot create new applications on Deezer Developers and there is no guarantee existing applications will continue to work.
As a workaround consider:
* Using the alternative [Deezer Source that uses unofficial, internal API via ARL](./?deezerSource=arl)
* Connect Deezer with Last.fm and then use [Last.fm as a Source](/configuration/sources/lastfm-source)
:::
Create a new application at [Deezer Developers](https://developers.deezer.com/myapps)
* Application Domain must be the same as your multi-scrobbler domain. Default is `localhost:9078`
* Redirect URL must end in `deezer/callback`
* Default would be `http://localhost:9078/deezer/callback`
After application creation you should have credentials displayed in the "My Apps" dashboard. You will need:
* **Application ID**
* **Secret Key**
* **Redirect URL** (if not the default)
**If no access token is provided...**
After starting multi-scrobbler with credentials in-place open the dashboard (`http://localhost:9078`) and find your Deezer source. Click **(Re)authenticate and (re)start polling** to start the login process. After login is complete polling will begin automatically.
###### Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-----------------------------------------|-----------------------------------|
| `DEEZER_CLIENT_ID` | Yes | | Your **Application ID** |
| `DEEZER_CLIENT_SECRET` | Yes | | Your **Secret Key** |
| `DEEZER_REDIRECT_URI` | No | `http://localhost:9078/deezer/callback` | URI must end in `deezer/callback` |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerDeprecatedConfig}</CodeBlock>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={DeezerDeprecatedConfig} name="deezer"/>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
</Tabs>
</TabItem>
</Tabs>
@@ -0,0 +1,159 @@
---
title: Google Cast (Chromecast)
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 ChromecastConfig from '!!raw-loader!@site/../config/chromecast.json.example';
If your media device can be **Cast** to using this button ![Chromecast Icon](https://upload.wikimedia.org/wikipedia/commons/2/26/Chromecast_cast_button_icon.svg) on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play.
:::note
Google Cast support is **experimental**. You may experience crashes and errors while using this Source. Please open an issue if you experience problems and include all information detailed in the issue template to help debug your issue.
:::
:::note
This source relies on common, **basic** music data provided by the cast device which will always be less exhaustive than data parsed from full source integrations. If there is an existing [Source](/configuration/sources) it is recommended to configure for it and blacklist the app on Google Cast, rather than relying solely on Google Cast for scrobbling.
:::
#### Connecting Devices
Cast devices can be manually configured using file-based configuration OR automatically discovered using **mDNS.**
###### mDNS Discovery
The host machine running multi-scrobbler must be configured to allow [mDNS traffic on port 5353/UDP](https://book.hacktricks.xyz/network-services-pentesting/5353-udp-multicast-dns-mdns).
<details>
<summary>OS Specific Instructions</summary>
<Tabs>
<TabItem value="linux" label="Linux">
**Docker**
The host machine must have [avahi-daemon](https://avahi.org/) running to circumvent limitations with DNS resolution due to musl in Alpine. Most major linux distributions package avahi and many have it built-in. Once avahi is running you must pass D-Bus and the avahi daemon socket to your container like so:
```
docker run ... -v /var/run/dbus:/var/run/dbus -v /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket ... foxxmd/multi-scrobbler
```
**Flatpak/Nodejs**
No additional steps are required.
</TabItem>
<TabItem value="windows" label="Windows">
**Docker**
Unsupported at this time.
**Nodejs**
No additional steps are required.
</TabItem>
</Tabs>
</details>
#### What Media Does MS Scrobble?
Cast devices report what type of media the current activity is [(see `metadata` property here)](https://developers.google.com/cast/docs/media/messages#MediaInformation). The reported type is dependent on the application playing the media to correctly report it, the cast device does not magically know what the media is. If an application does not report a type it is always classified as `unknown`.
**By default, MS will only track media that is reported as `MusicTrack`.**
##### Allow Unknown Media Type
Media with an Unknown (`Generic`) media type can be explicitly allowed by setting `"allowUnknownMedia": true` in the file-based configuration. This can also be configured to only allow unknown media types for specific applications by using a list of application names.
<details>
<summary>Example</summary>
```json5 title="chromecast.json"
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
// only allow unknown if app name contains any of these phrases
"allowUnknownMedia": ["smarttube", "default media receiver"]
},
}
]
```
</details>
##### Forcing Media Tracking
MS can be forced to track media from an application regardless of media type. This is useful if an application incorrectly reports a media type you are sure should be music. Set `"forceMediaRecognitionOn"` in the file-based configuration to a list of application names that should always be tracked.
<details>
<summary>Example</summary>
```json5 title="chromecast.json"
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
// media from applications that contains these phrases will always be tracked, regardless of media type reported
"forceMediaRecognitionOn": ["smarttube", "default media receiver"]
},
}
]
```
</details>
#### Cast Troubleshooting
Please include any/all logs with raw output if there are any errors encountered as this is critical to diagnosing issues.
To diagnose bad/incomplete track information or strange MS player behavior please turn on **payload logging** and include log output of the source running to help diagnose this issue. Either enable in config using the below example OR enable [Debug Mode.](/configuration#debug-mode)
<details>
<summary>Example</summary>
```json5 title="chromecast.json"
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
//...
},
"options": {
"logPayload": true
}
}
]
```
</details>
## Configuration
<Config config="ChromecastSourceConfig" fileContent={ChromecastConfig} name="chromecast">
:::note
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
:::
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------------------------------------------------------|
| CC_ENABLE | No | | Set to 'true' to enable Cast monitoring without needing to define other ENVs |
| CC_WHITELIST_DEVICES | No | | Only scrobble from these Cast devices. Comma-delimited list. EX mini-home, family-tv |
| 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 |
</Config>
@@ -0,0 +1,70 @@
---
title: Icecast
toc_min_heading_level: 2
toc_max_heading_level: 5
description: Icecast Source Configuration
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import IcecastConfig from '!!raw-loader!@site/../config/icecast.json.example';
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
Parse metadata from any Icecast Station (streams) to use for scrobbling.
:::note
There is no standard format for presenting track information in an Icecast stream and the majority of streams that do include metadata only use a single string (`StreamTitle`) from which metadata can be parsed.
Multi-scrobbler will do its best to parse Artist/Album/Track information from this data but the quality and consistency for this is solely based on how the Icecast broadcaster chooses to format their tracks.
:::
Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, **automatic scrobbling for Icecast Sources is off by default.** Use **Should Scrobble Behavior** to initiate scrobbling or configure automatic scrobble.
<ShouldScrobble filename="icecast"/>
:::tip[Scrobbling Threshold]
Icecast does not report track lengths so multi-scrobbler cannot determine if it should scrobble based on percent played. You should instead set a shorter "listened to" **duration** using [Scrobble Thresholds.](/configuration/sources#scrobble-thresholds)
:::
#### URL and Sources
The URL should be the URL you would use to stream audio.
Additional sources are supported based on those found in [`icecast-metadata-stats`](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#supported-status-apis). To use these with automatic parsing [based on the regular URL](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options) include a `sources` array in your config the same way as shown in `icecast-metadata-stats`:
<details>
<summary>Example</summary>
```json5 title="config.json"
[
{
"name": "myIcy",
"enable": true,
"data": {
"url": "http://myIcecast.com/radio",
"sources": ["icy", "ogg", "sevenhtml"]
}
}
]
```
</details>
To manually set these sources include the same [`*Endpoint` options](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options) found in `icecast-metadata-stats`.
If no `sources` are configured for multi-scrobbler it will automatically try to use `icy` and `ogg`.
## Configuration
<Config config="IcecastSourceConfig" fileContent={IcecastConfig} name="icecast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ----------- |
| `ICECAST_URL` | Yes | | Icecast URL |
</Config>
@@ -0,0 +1,69 @@
---
title: Jellyfin
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 JellyfinConfig from '!!raw-loader!@site/../config/jellyfin.json.example';
Must be using Jellyfin 10.7 or greater
* Create an **API Key** for multi-scrobbler
* In the Jellyfin desktop web UI Navigate to -> Administration -> Dashboard -> API Keys (`http://YOUR_JELLYIN_URL/web/index.html#!/apikeys.html`)
* Click `+` button and create a new key with **App name** `multi-scrobbler`
* Copy the created API Key value for use in configuration below
It is **recommended** to use API Key + username but if you are not an admin for your Jellyfin instance you can also authenticate with your Jellyfin username and **password.**
<DetailsAdmo type="tip" summary="Important Defaults">
By default...
* multi-scrobbler will **only** scrobble for the user authenticated with the API.
* Allowed Users (`usersAllow` or `JELLYFIN_USERS_ALLOW`) are only necessary if you want to scrobble for additional users.
* multi-scrobbler will **only** scrobble media found in Jellyfin libraries that were labelled as **Music.**
* `librariesAllow` or `JELLYFIN_LIBRARIES_ALLOW` will override this
* OR use `additionalAllowedLibraryTypes` to allow more *library* types (like those labelled as `mixed` or `book` for audiobooks)
* multi-scrobbler will **only** scrobble media Jellyfin detects as **Audio**.
* To allow other media types to be scrobbled set `allowMediaTypes` or `JELLYFIN_MEDIATYPES_ALLOW` with a list of [official types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22).
* One or more of: `Unknown`, `Video`, `Audio`, `Photo`, `Book`
* If you use this setting **you must explicitly specify `Audio`**, it will no longer be provided as a 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
<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>
@@ -0,0 +1,58 @@
---
title: JRiver
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 JriverConfig from '!!raw-loader!@site/../config/jriver.json.example';
In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can can be in the JRiver GUI:
* Tools -> Options -> Media Network
* Check `Use Media Network to share this library...`
* If you have `Authentication` checked you will need to provide the **Username** and **Password** in the ENV/File configuration below.
### URL
The URL used to connect to JRiver, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
```
Syntax => [protocol]://[hostname]:[port]/[path]
Default => http://localhost:52199/MCWS/v1/
```
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
:::tip[Jriver Settings]
* Make sure the port number matches what is found in `Advanced` section in the Media Network options.
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
:::
<details>
<summary>URL Transform Examples</summary>
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to JRiver:
| `url` | Transformed URL |
|------------------------------------------|---------------------------------------------|
| (none set) | `http://localhost:52199/MCWS/v1/` |
| `jriver.mydomain.com` | `http://jriver.mydomain.com:52199/MCWS/v1/` |
| `192.168.0.101:3456` | `http://192.168.0.101:3456/MCWS/v1/` |
| `mydomain.com:80/jriverReverse/MCWS/v1/` | `mydomain.com:80/jriverReverse/MCWS/v1/` |
</details>
## Configuration
<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 |
</Config>
@@ -0,0 +1,59 @@
---
title: Kodi
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 KodiConfig from '!!raw-loader!@site/../config/kodi.json.example';
import Config from '@site/src/components/GenericConfiguration.mdx';
In order for multi-scrobbler to communicate with Kodi you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can be done in the Kodi GUI:
* Settings -> Services -> Control
* Check `Allow remote control via HTTP`
* Ensure you have a **Username** and **Password** set, you will need to provide them in the ENV/File configuration below.
### URL
The URL used to connect to Kodi, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
```
Syntax => [protocol]://[hostname]:[port]/[path]
Default => http://localhost:8080/jsonrpc
```
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
:::tip[Kodi Settings]
* Make sure the port number matches what is found in **Port** in the Control section mentioned above.
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
:::
<details>
<summary>URL Transform Examples</summary>
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to Kodi:
| `url` | Transformed URL |
|---------------------------------------|----------------------------------------------|
| (none set) | `http://localhost:8080/jsonrpc` |
| `kodi.mydomain.com` | `http://kodi.mydomain.com:8080/jsonrpc` |
| `192.168.0.101:3456` | `http://192.168.0.101:3456/jsonprc` |
| `mydomain.com:80/kodiReverse/jsonrpc` | `http://mydomain.com:80/kodiReverse/jsonrpc` |
</details>
## Configuration
<Config config="KodiSourceConfig" fileContent={KodiConfig} name="kodi">
| Environmental Variable | Required | Default | Description |
|------------------------|----------|-------------------------------|----------------------------|
| KODI_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server |
| KODI_USERNAME | No | | The username set |
| KODI_PASSWORD | No | | The password set |
</Config>
@@ -0,0 +1,33 @@
---
title: Koito
description: Koito (Source)
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 KoitoConfig from '!!raw-loader!@site/../config/koito.json.example';
This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured [Clients.](/configuration/clients)
:::tip[Other Uses]
To _scrobble to_ a Koito server, create a [Koito (Client)](/configuration/clients/koito)
:::
See the [Koito (Client)](/configuration/clients/koito) configuration for general setup. The only difference for **Source** configuration:
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
#### Configuration
<Config config="KoitoSourceConfig" fileContent={KoitoConfig} name="koito">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | --------------------------------- |
| `SOURCE_KOITO_TOKEN` | Yes | | API Key from your Koito Account |
| `SOURCE_KOITO_USER` | Yes | | Your Koito username |
| `SOURCE_KOITO_URL` | No | | The base URL for the Koito server |
</Config>
@@ -0,0 +1,46 @@
---
title: Lastfm (Endpoint)
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 LastfmEndpointConfig from '!!raw-loader!@site/../config/endpointlfm.json.example';
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server.
:::tip[Other Uses]
* To **scrobble to** Last.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
* To monitor and re-scrobble activity from your Last.fm account, create a [Last.fm (Source)](/configuration/sources/lastfm-source)
:::
### URL
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Last.fm scrobbles at
```
http://localhost:9078/2.0/
```
which is the "standard" Last.fm server path for scrobbling
Use a slug only if you need to setup multiple Last.fm Endpoint sources and cannot use different tokens.
If a slug is used then the URL will be:
```
http://localhost:9078/api/lastfm/mySlug
```
## Configuration
<Config config="LastFMEndpointSourceConfig" fileContent={LastfmEndpointConfig} name="endpointlfm">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `LFMENDPOINT_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
| `LFM_SLUG` | No | | (Optional) The URL suffix to use for accepting LFM scrobbles |
</Config>
@@ -0,0 +1,33 @@
---
title: Last.fm
description: Last.fm (Source)
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 LastfmConfig from '!!raw-loader!@site/../config/lastfm.json.example';
This Source **monitors your Last.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
:::tip[Other Uses]
* To **scrobble to** Last.fm/Libre.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
:::
To configure this Source use the same registration instructions and configuration data shown in [Last.fm (Client)](/configuration/clients/lastfm) with the difference being `"configureAs": "source"` defined in the File/AIO configuration below.
## Configuration
<Config config="LastfmSourceConfig" fileContent={LastfmConfig} name="lastfm">
| Environmental Variable | Required? | Default | Description |
| ---------------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
| `SOURCE_LASTFM_API_KEY` | Yes | | Api Key from your API Account |
| `SOURCE_LASTFM_SECRET` | Yes | | Shared secret from your API Account |
| `SOURCE_LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
| `SOURCE_LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
</Config>
@@ -0,0 +1,34 @@
---
title: Libre.fm
description: Libre.fm (Source)
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/librefm.json.example';
This Source **monitors your Libre.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
:::tip[Other Uses]
* To **scrobble to** Libre.fm, create a [Libre.fm (Client)](/configuration/clients/librefm)
:::
To configure this Source use the same registration instructions and configuration data shown in [Libre.fm (Client)](/configuration/clients/librefm) with the difference being `"configureAs": "source"` defined in the File/AIO configuration below.
## Configuration
<Config config="LibrefmSourceConfig" fileContent={JsonConfig} name="librefm">
| Environmental Variable | Required? | Default | Description |
| ----------------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `SOURCE_LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
| `SOURCE_LIBREFM_API_KEY` | No | `anything` | Optional API Key |
| `SOURCE_LIBREFM_SECRET` | No | `anything` | Optional shared secret |
| `SOURCE_LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
| `SOURCE_LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
| `SOURCE_LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
</Config>
@@ -0,0 +1,117 @@
---
title: Listenbrainz (Endpoint)
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 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.**
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]
* To **scrobble to** Listenbrainz, create a [Listenbrainz (Client)](/configuration/clients/listenbrainz)
* To monitor and re-scrobble activity from your Listenbrainz account, create a [Listenbrainz (Source)](/configuration/sources/listenbrainz-source)
:::
## Setup
### URL
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.
</DetailsAdmo>
<DetailsAdmo type="note" summary="Some Services Require HTTPS">
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.
</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
<Config config="ListenbrainzEndpointSourceConfig" fileContent={ListenbrainzEndpointConfig} name="endpointlz">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `LZENDPOINT_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
| `LZE_TOKEN` | No | | LZ "Authentication Token" you provided to the scrobbling application |
| `LZE_SLUG` | No | | (Optional) The URL suffix to use for accepting LZ scrobbles |
</Config>
@@ -0,0 +1,34 @@
---
title: Listenbrainz
description: Listenbrainz (Source)
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 ListenbrainzConfig from '!!raw-loader!@site/../config/listenbrainz.json.example';
This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients.
:::tip[Other Uses]
* To **scrobble to** a Listenbrainz server, create a [Listenbrainz (Client)](/configuration/clients/listenbrainz)
* To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
:::
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/).
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
## Configuration
<Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
| `SOURCE_LZ_TOKEN` | Yes | | User token from your LZ profile |
| `SOURCE_LZ_USER` | Yes | | Your LZ username |
| `SOURCE_LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
</Config>
@@ -0,0 +1,29 @@
---
title: Maloja
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 MalojaConfig from '!!raw-loader!@site/../config/maloja.json.example';
:::tip[Other Uses]
To _scrobble to_ a Maloja server, create a [Maloja (Client)](/configuration/clients/maloja)
:::
See the [Maloja (Client)](/configuration/clients/maloja) configuration for general setup. The only difference for **Source** configuration:
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
## Configuration
<Config config="MalojaSourceConfig" fileContent={MalojaConfig} name="maloja">
| Environmental Variable | Required? | Default | Description |
| ----------------------- | --------- | ------- | ----------------------------- |
| `SOURCE_MALOJA_URL` | Yes | | Base URL of your installation |
| `SOURCE_MALOJA_API_KEY` | Yes | | Api Key |
</Config>
@@ -0,0 +1,102 @@
---
title: Mopidy
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 MopidyConfig from '!!raw-loader!@site/../config/mopidy.json.example';
Mopidy is a headless music server that supports playing music from many [standard and non-standard sources such as Pandora, Bandcamp, and Tunein.](https://mopidy.com/ext/)
multi-scrobbler can scrobble tracks played from any Mopidy backend source, regardless of where you listen to them.
:::note
You must have [Mopidy-HTTP extension](https://mopidy.com/ext/http) enabled for this integration to work.
:::
#### URL
The URL used to connect to the Mopidy server, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
```
Syntax => [protocol]://[hostname]:[port]/[path]
Default => ws://localhost:6680/mopidy/ws
```
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
:::tip[Mopidy Configuration]
Make sure the hostname and port number match what is found in the Mopidy configuration file `mopidy.conf`:
```ini
[http]
hostname = localhost
port = 6680
```
:::
<details>
<summary>URL Transform Examples</summary>
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to Mopidy:
| `url` | Transformed URL |
|--------------------------------|--------------------------------------------|
| (none set) | `ws://localhost:6680/mopidy/ws` |
| `mopidy.mydomain.com` | `ws://mopidy.mydomain.com:6680/mopidy/ws/` |
| `192.168.0.101:3456` | `ws://192.168.0.101:3456/mopidy/ws/` |
| `mopidy.mydomain.com:80/MOPWS` | `ws://mopidy.mydomain.com:80/MOPWS` |
</details>
### URI Blacklist/Whitelist
If you wish to disallow or only allow scrobbling from some sources played through Mopidy you can specify these using `uriBlacklist` or `uriWhitelist` in your config. multi-scrobbler will check the list to see if any string matches the START of the `uri` on a track. If whitelist is used then blacklist is ignored. All strings are case-insensitive.
<details>
<summary>Example</summary>
```json
{
"uriBlacklist": ["soundcloud"]
}
```
Will prevent multi-scrobbler from scrobbling any Mopidy track that start with a `uri` like `soundcloud:song:MySong-1234`
</details>
### Album Blacklist
For certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use "Soundcloud" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake Album name to this list. Multi-scrobbler will still scrobble the track, just without the bad data. All strings are case-insensitive.
<details>
<summary>Example</summary>
```json
{
"albumBlacklist": ["SoundCloud", "Mixcloud"]
}
```
If a track would be scrobbled like
```
Album: Soundcloud, Track: My Cool Track, Artist: A Cool Artist
```
then multi-scrobbler will instead scrobble
```
Track: My Cool Track, Artist: A Cool Artist
```
</details>
## Configuration
<Config config="MopidySourceConfig" fileContent={MopidyConfig} name="mopidy">
No ENV support
</Config>
@@ -0,0 +1,28 @@
---
title: MPD (Music Player Daemon)
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 MPDConfig from '!!raw-loader!@site/../config/mpd.json.example';
MS communicates with MPD using the [TCP client connection.](https://mpd.readthedocs.io/en/stable/user.html#client-connections)
You should uncomment/create the following settings in your mpd config:
```
bind_to_address "any" # or a specific ipv4/v6 address
port "6600"
```
## Configuration
<Config config="MPDSourceConfig" fileContent={MPDConfig} name="mpd">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|------------------|-------------|
| `MPD_URL` | No | `localhost:6600` | |
| `MPD_PASSWORD` | No | | |
</Config>
@@ -0,0 +1,79 @@
---
title: MPRIS
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 MPRISConfig from '!!raw-loader!@site/../config/mpris.json.example';
[MPRIS](https://specifications.freedesktop.org/mpris-spec/latest) is a standard interface for communicating with Music Players on **linux operating systems.**
If you run Linux and have a notification tray that shows what media you are listening to, you likely have access to MPRIS.
![Notification Tray](../mpris.jpg)
multi-scrobbler can listen to this interface and scrobble tracks played by **any media player** that communicates to the operating system with MPRIS.
##### Host Setup
MPRIS communication requires multi-scrobbler to have access to the host's dbus-daemon.
<Tabs groupId="dbus" queryString>
<TabItem value="local" label="Local">
If multi-scrobbler is running as a [**Local Installation**](../../installation/installation.mdx#nodejs), directly with node/npm, on the same host as MPRIS then no setup is required.
Remote setup is also possible, see the **Remote Unix Socket** section in the Docker tab.
</TabItem>
<TabItem value="docker" label="Docker">
If multi-scrobbler is running as a [**Docker Container**](../../installation/installation.mdx#docker) some modifications are required...
##### 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?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"
environment:
- DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/UID/bus
volumes:
- /run/user/UID/bus:/run/user/UID/bus:ro
```
##### Remote Unix Socket
:::warning
This method is **insecure.** You should not expose dbus over a network unless access to the port is restricted and the network is trusted.
:::
Use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from the host's dbus socket to a listening TCP port.
To make dbus for user with uid `1000` available on port `6644`:
```shell
socat TCP-LISTEN:6644,reuseaddr,fork UNIX-CONNECT:/run/user/1000/bus
```
Then, add the following value to multi-scrobbler's ENVs:
```
DBUS_SESSION_BUS_ADDRESS=tcp:host=yourHostIP,port=6644
```
</TabItem>
</Tabs>
## Configuration
<Config config="MPRISSourceConfig" fileContent={MPRISConfig} name="mpris">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|----------------------------------------------------------------------------------|
| MPRIS_ENABLE | No | | Use MPRIS as a Source (useful when you don't need any other options) |
| MPRIS_BLACKLIST | No | | Comma-delimited list of player names not to scrobble from |
| MPRIS_WHITELIST | No | | Comma-delimited list of players names to ONLY scrobble from. Overrides blacklist |
</Config>
@@ -0,0 +1,38 @@
---
title: Musikcube
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 MusikcubeConfig from '!!raw-loader!@site/../config/musikcube.json.example';
In order to use [Musikcube](https://musikcube.com) configure it to accept [websocket connections](https://github.com/clangen/musikcube/wiki/remote-api-documentation) in **server setup**:
* Enable the **Metadata Server**
* Set a **Password**
Both of these settings are found in _Musikcube -> (s)ettings -> server setup_
![Server Setup](../musikcube.jpg)
The URL used by MS has the syntax:
```
[ws|wss]://HOST:[PORT]
```
The **port** is the same as shown in the server setup screenshot from above, under **metadata server enabled**. If no port is provided to MS it will default to `7905`.
If no URL is provided to MS it will try to use `ws://localhost:7905`
## Configuration
<Config config="MusikcubeSourceConfig" fileContent={MusikcubeConfig} name="musikcube">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-----------------------|--------------------------------------|
| `MC_URL` | No | `ws://localhost:7905` | Use port set for **metadata server** |
| `MC_PASSWORD` | Yes | | |
</Config>
+207
View File
@@ -0,0 +1,207 @@
---
title: Plex
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 AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example';
import PlexConfig from '!!raw-loader!@site/../config/plex.json.example';
Find your [**Plex Token**](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) and make note of the **URL** and **Port** used to connect to your Plex instance.
<details>
<summary>Allowed Users and Defaults</summary>
**Multi-scrobbler will automatically scrobble for these users by default:**
* The User authenticated with the Plex Token
* and the **Local User**
The Local User (`PLEX_LOCAL_USER`) is how Plex identifies anyone directly accessing the Plex UI from a local IP (who does not need to login).
To allow MS to scrobble for other users use `usersAllow` or `PLEX_USERS_ALLOW` (env) from the below configuration docs. However, because you are overriding the default settings you must also explicitly list the authenticated user and the Local User if you want them to also be able to scrobble.
<details>
<summary>Examples</summary>
###### Defaults
If `usersallow` and `PLEX_USERS_ALLOW` are not defined then the Plex Token authenticated User and Local User will be scrobbled for.
###### Only A Specific User
* `"usersallow": ["SomeUser"]` or
* `PLEX_USERS_ALLOW: SomeUser`
Only the Plex user `SomeUser` will be scrobbled for. The Plex Token authenticated user and the Local User will not be scrobbled for.
###### A Specific User + Defaults
(Assuming the plex authenticated user is `FoxxMD`)
* `"usersallow": ["FoxxMD", "PLEX_LOCAL_USER", "SomeUser"]` or
* `PLEX_USERS_ALLOW: FoxxMD,PLEX_LOCAL_USER,SomeUser`
The Plex user SomeUser, the Plex Token authenticated user (FoxxMD) and the Local User will be scrobbled for.
</details>
</details>
<details>
<summary>Allowed Libraries and Defaults</summary>
By default multi-scrobbler will only scrobble media found in Plex libraries that are labelled as **Music.**
* `librariesAllow` or `PLEX_LIBRARIES_ALLOW` will override this
</details>
<DetailsAdmo type="tip" summary="Use Plex metadata with Musicbrainz for better scrobbling">
<h3>Matching With Plex</h3>
In Plex, if the **Plex Music** Agent is set on a Music Library then Plex attempts to match your music with Musicbrainz when it is first imported.
<img src={require('/img/plex_music_agent.jpg').default} height="400"/>
Plex uses ID3/metdata tags to match or falls back to matching based on file name and folder names. If it cannot find a match it can be fixed through a **Fix Match** option in the album/track context menu. See [plex documentation for more information](https://support.plex.tv/articles/correcting-your-music-content-matches/).
You can verify if an Item has been matched by:
* Clicking on the Item context menu (...)
* Get Info
* View XML
If a node like this is present then it has been matched: `<Guid id="mbid://4e631558-13b0..."/>`
<h3>Using Musicbrainz With Multi-Scrobbler</h3>
The MBID from the above `<Guid>` node is a [Track](https://musicbrainz.org/doc/Track), a special type of ID that specifically identifies a song on a specific album. If this is present then Multi-scrobbler can use it to gaurantee your scrobble, and all its metadata, is matched 1:1 with what is in your Plex library when using the [Musicbrainz Stage](/configuration/transforms/musicbrainz) to enrich your scrobble.
To configure MS to use Musicbrainz with your Plex metadata use one of the methods below:
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
Add to your multi-scrobbler docker compose file `environment` section for [Musicbrainz ENV Configuration](/configuration/transforms/musicbrainz#env-configuration):
```yaml
# a real email that Musicbrainz can use to contact you in case of issues
- MB_CONTACT=myEmail@myEmailProvider.com
- MB_PRESETS=id,default
- PLEX_TRANSFORMS=musicbrainz
```
</TabItem>
<TabItem value="file" label="File">
Your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "musicbrainz",
"name": "MyMB",
"data": {
"apis": [
{
// a real email that Musicbrainz can use to contact you in case of issues
"contact": "myEmail@myEmailProvider.com"
}
]
}
}
]
}
```
In your [File Config](/configuration?configType=file#configuration-types):
```json5 title="plex.json"
[
{
"name": "MyPlex",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "musicbrainz",
"name": "MyMB"
"searchOrder": ["mbidrecording", "basicOrIds", "basic"]
}
]
}
}
}
]
```
</TabItem>
<TabItem value="aio" label="File AIO">
Your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "musicbrainz",
"name": "MyMB",
"data": {
"apis": [
{
// a real email that Musicbrainz can use to contact you in case of issues
"contact": "myEmail@myEmailProvider.com"
}
]
}
}
]
// ...
"sources": [
// ...
{
"name": "MyPlex",
"type": "plex",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "musicbrainz",
"name": "MyMB",
"searchOrder": ["mbidrecording", "basicOrIds", "basic"]
}
]
}
}
}
]
}
```
</TabItem>
</Tabs>
</DetailsAdmo>
#### Configuration
<Config config="PlexApiSourceConfig" fileContent={PlexConfig} name="plex">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ---------------------------------------------------------------------- |
| `PLEX_URL` | **Yes** | | The URL of the Plex server IE `http://localhost:32400` |
| `PLEX_TOKEN` | **Yes** | | The **Plex Token** to use with the API |
| `PLEX_USERS_ALLOW` | No | | Comma-separated list of usernames (from Plex) to scrobble for |
| `PLEX_USERS_BLOCK` | No | | Comma-separated list of usernames (from Plex) to disallow scrobble for |
| `PLEX_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
| `PLEX_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
| `PLEX_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
| `PLEX_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
</Config>
@@ -0,0 +1,40 @@
---
title: Rocksky
description: Rocksky (Source)
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/rocksky.json.example';
:::tip[Other Uses]
* To **scrobble to** Rocksky, create a [Rocksky (Client)](/configuration/clients/rocksky)
:::
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
This Source monitors the Rocksky scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients.
Learn more about how Rocksky scrobbles work in the [Rocksky (Client)](/configuration/clients/rocksky) docs.
## Setup
Follow the **Setup** instructions in the [Rocksky (Client)](/configuration/clients/rocksky#setup) docs to get access to your Bluesky PDS repository and scrobble data.
## Configuration
See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration for general setup. The only difference for **Source** configuration:
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
<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` |
| `SOURCE_ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard |
</Config>
@@ -0,0 +1,100 @@
---
title: Sonos
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/sonos.json.example';
Multi-scrobbler can use the [Sonos](https://www.sonos.com) [local API](https://docs.sonos.com/docs/control) to monitor what your Sonos system is playing.
MS supports scrobbling simultaneous, different Plays based on what each Group/Zone is playing.
<DetailsAdmo type="tip" summary="Use Musicbrainz for better scrobbling">
For some services, including Spotify, Sonos only reports the **first** artist for a Track. You will most likely want to enable [Musicbrainz scrobble enhancements](/configuration/transforms/musicbrainz) so that your plays have correct information, including all artists, when scrobbled.
</DetailsAdmo>
<DetailsAdmo type="note" summary="Avoid Sonos activity through other Sources">
Activity reporting for **other [Sources](/configuration/sources)** does not work well **when played through Sonos.**
If you have other Sources configured for Multi-Scrobbler that you may play *through Sonos* you should try to add Sonos to the "deny" list for that Source so that it does not try to monitor Sonos activity. Check the documentation for each individual Source to see how to configure this exclusion.
For example, to prevent the [Plex](/configuration/sources/plex) Source from monitoring audio played through Sonos, add Sonos to the "block" list ENV EX: `PLEX_DEVICES_BLOCK=sonos`
</DetailsAdmo>
<details>
<summary>How to report issues</summary>
If your Sonos Source is logging errors or the scrobble data is not as expected please [create a bug report](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=01-bug-report.yml) **after** enabling the following debug logging options based on the config type you have:
<Tabs groupdId="sonosBugConfig">
<TabItem value="env" label="ENV">
Enable [Debug Mode](/configuration#debug-mode) using `DEBUG_MODE=true`
</TabItem>
<TabItem value="file" label="File">
Enable `logPayload` and `logFitlerFailure` in your `sonos.json`, or AIO file, config:
```json
[
{
"enable": true,
"name": "MySonos",
"data": {
"host": "192.168.0.150"
},
"options": {
"logFilterFailure": "warn",
"logPayload": true
}
}
]
```
</TabItem>
</Tabs>
Additional logging for the Sonos library may be enabled by including this environmental variable (regardless of config type):
```
DEBUG=sonos:*
```
The above changes:
* Triggers multi-scrobbler to log the raw data recieved from Sonos when a new track/device/service is seen for the first time
* Trigger multi-scrobbler to log the reason it considers a play from Sonos as invalid (and will not monitor it)
* Logs low-level activity from the Sonos library
Please include these additional logs when creating a bug report. They are essential to troubleshooting your issue.
</details>
### Filtering Activity
The **optional** `devices` and `groups` related properties found in the config below can be used to include or exclude Sonos devices/groups by name, case-insensitive.
## Configuration
You **must** define the IP address of at least one Sonos/accessory in order for Multi-scrobbler to connect to your Sonos system.
<Config config="SonosSourceConfig" fileContent={JsonConfig} name="sonos">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | :--------------------------------------------------------------------- |
| `SONOS_HOST` | Yes | | The IP address of any active Sonos device/accessory EX `192.168.0.150` |
| `SONOS_DEVICES_ALLOW` | No | | Comma-separated list of Sonos device names to scrobble from |
| `SONOS_DEVICES_BLOCK` | No | | Comma-separated list of Sonos device names to disallow scrobbles from |
| `SONOS_GROUPS_ALLOW` | No | | Comma-separated list of device group names to scrobble from |
| `SONOS_GROUPS_BLOCK` | No | | Comma-separated list of devices group names to disallow scrobbles from |
</Config>
@@ -0,0 +1,159 @@
---
sidebar_position: 2
title: Overview
description: Scrobble Sources
toc_max_heading_level: 4
---
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
## Sources
| Name | Networking | Scrobble SOT | [Multi Device/User](#multi-deviceuser) | [Scrobble Destination](#limiting-scrobble-destination) | [Thresholds](#scrobble-thresholds) | [Should Scrobble](#should-scrobble-behavior) |
| :---------------------------------------------------------------------- | :------------------------------------------------------- | :-------------------------------------------------- | :------------------------------------- | :----------------------------------------------------- | :--------------------------------- | :------------------------------------------- |
| [Azuracast](/configuration/sources/azuracast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ✅ |
| [Deezer](/configuration/sources/deezer) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ✅ | ✅ | ❌ | ❌ |
| [Google Cast (Chromecast)](/configuration/sources/google-cast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [Icecast](/configuration/sources/icecast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ✅ |
| [Jellyfin](/configuration/sources/jellyfin) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [JRiver](/configuration/sources/jriver) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Kodi](/configuration/sources/kodi) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Koito](/configuration/sources/koito-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Last.fm](/configuration/sources/lastfm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Libre.fm](/configuration/sources/librefm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint) | [Ingress](./?sourceComm=ingress#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [ListenBrainz](/configuration/sources/listenbrainz-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [ListenBrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint) | [Ingress](./?sourceComm=ingress#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Maloja](/configuration/sources/maloja) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Mopidy](/configuration/sources/mopidy) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [MPD (Music Player Daemon)](/configuration/sources/mpd) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Musikcube](/configuration/sources/musikcube) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Plex](/configuration/sources/plex) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [Rocksky](/configuration/sources/rocksky-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Sonos](/configuration/sources/sonos) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [Spotify](/configuration/sources/spotify) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [Subsonic-compatible APIs](/configuration/sources/subsonic) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [teal.fm](/configuration/sources/tealfm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [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
### Types of Sources
The Sources implemented in multi-scrobbler can be broken down into two categories.
#### By Communication Method
How does this Source get data from the service/application?
<Tabs groupId="sourceComm" queryString>
<TabItem value="active" label="Active">
The MS Source makes network requests to the service which returns a response with data MS can use to monitor listening activity or scrobbles.
</TabItem>
<TabItem value="ingress" label="Ingress">
The MS Source waits for the service/application to send data to multi-scrobbler.
These types of Sources require that multi-scrobbler is accessible to the service, generally through an open port or reverse proxy.
</TabItem>
</Tabs>
#### By Data Source of Truth
How does MS determine if/when a song has been played and is scrobbable?
<Tabs groupId="sot" queryString>
<TabItem value="activity" label="Listening Activity">
These Sources expose some kind of real-time listening data for users, like:
* player state (paused, stopped, playing)
* player position (1:50/2:40)
* current listened duration (Track A playing for 1m:20s)
For theses Sources multi-scrobbler keeps track of the real-time state of your activity and scrobbles a track after it meets certain [scrobble thresholds](#scrobble-thresholds).
</TabItem>
<TabItem value="history" label="Listening History">
Some Sources expose their own version of "this track has been scrobbled."
* Listenbrainz, Koito, and Last.fm are all scrobble services that have "definitive" histories of scrobbled tracks.
For these Sources multi-scrobbler monitors the listening history data and scrobbles when it sees a new entry in the list.
</TabItem>
</Tabs>
### Multi-Device/User
Some Sources report which User is playing a track and/or on what device the track is being played. These Sources can be filtered with configuration to only monitor activity for specific devices or users.
### Limiting Scrobble Destination
To limit which Clients a Source will scrobble to use the `clients` property in a Source's [File/AIO configuration](/configuration?configType=file#configuration-types) to specify the **name** property of the Clients that it should scrobble to:
```json5 title="jellyfin.json"
{
"name": "MyJellyfin",
"clients": ["myMaloja","koitoA","listenbrainzFoo"]
"data": {
// ...
},
}
```
:::note
If `clients` is empty (`"clients": []`) or is omitted then the default is for the Source to scrobble to **all** configured Clients.
:::
:::note
If a Client is configured using [ENV](/configuration?configType=env#configuration-types) then it is given a generated name. Check the multi-scrobbler Dashboard to find their name in a subheader under the Client type. Example:
![Client Name Subheader](../client-subheader.png)
Otherwise, use the `name` property you gave the client in it's File/AIO config.
:::
### Scrobble Thresholds
<ScrobbleThreshold filename="spotify"/>
These can also be configured **globally** for all Sources using ENVs:
```
SOURCE_SCROBBLE_DURATION=40
SOURCE_SCROBBLE_PERCENT=20
```
### Should Scrobble Behavior
:::note
This feature is currently available only for some Sources. Check each Source's documentation to see if it supported.
:::
For _most_ Sources multi-scrobbler can determine if a user is listening and scrobble automatically. For Sources which multi-scrobbler cannot do this or require additional configuration, a user can indicate to MS that it should or should not scrobble. This action can be taken on the Web Dashboard by clicking the link next to **Should Scrobble** found in each Source's status card.
Additionally, for these Sources, the default behavior for Scrobbling can be explicitly set by adding the following value to a Source's `options` in [file or aio config](/configuration?configType=file#configuration-types):
```json5 title="azuracast.json"
{
"data": {
// ...
},
"options": {
// set to true to always scrobble automatically
// set to false to never scrobble automatically
"systemScrobble": true
}
}
```
@@ -0,0 +1,119 @@
---
title: Spotify
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 SpotifyConfig from '!!raw-loader!@site/../config/spotify.json.example';
import IconWarning from '@theme/Admonition/Icon/Warning';
<DetailsAdmo type="note" summary="Spotify and Automix">
If your Spotify player has [Automix](https://community.spotify.com/t5/FAQs/What-is-Automix/ta-p/5257278) enabled and Spotify uses it for your playlist/queue then MS cannot accurately determine when a track will end. This is because the track is "mixed" in your queue with a shorter play time than its actual length and [Spotify does not report this modified play time in its API.](https://community.spotify.com/t5/Spotify-for-Developers/Wrong-duration-ms-of-track-with-Automix/m-p/5429147) This **does not affect MS's ability to scrobble** from Spotify but it will affect the accuracy of the duration MS reports was played.
</DetailsAdmo>
#### 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.
1. **Login to https://developer.spotify.com** with your existing Spotify account and accept Developer Terms
2. Navigate to your Spotify Developer [Dashboard](https://developer.spotify.com/dashboard) and start the **Create App** process
3. Determine the correct **Redirect URI** to use and set it in your App settings
<details>
<summary>Redirect URI Instructions</summary>
A Redirect URI is the URL that Spotify will navigate your **browser** to after you complete authorization. The URL will contain the code necessary for multi-scrobbler to get a Spotify access token.
Spotify [**no longer allows insecure URIs**](https://developer.spotify.com/documentation/web-api/tutorials/migration-insecure-redirect-uri) (start with `http://`) unless the address is `127.0.0.1` -- so `localhost` and internal IPs (`192.168.0.xxx`) no longer work.
Use **one** of the following methods to specify a valid Redirect URI and complete Spotify authentication with multi-scrobbler:
<Tabs groupId="spotifyRedirect" queryString>
<TabItem value="https" label="HTTPS">
**Use a domain you control, with SSL certificates/HTTPS enabled, to create a valid redirect URI.**
For example, if you are already running multi-scrobbler behind a reverse proxy (nginx/traefik/caddy) at `https://scrobbler.mydomain.com` then set the Spotify Redirect URI for your Spotify App, and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV, to `https://scrobbler.mydomain.com/callback`
Alternatively, if multi-scrobbler is not accessible behind your domain, then use the [**Echo**](./?spotifyRedirect=echo) method with your own domain instead of 127.0.0.1.
</TabItem>
<TabItem value="local" label="Run MS Locally">
**Run multi-scrobbler, configured for a Spotify Source, from the same machine as your browser in order to get credentials.**
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?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.**
1. Create a container on the same machine as your browser using the [`mendhak/http-https-echo:36`](https://github.com/mendhak/docker-http-https-echo) image. This container runs a simple HTTP server that echos back any request it recieves.
```shell
docker run --rm -p 127.0.0.1:5079:8080 mendhak/http-https-echo:36
```
2. Set the Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV as `http://127.0.0.1:5079/callback`
3. In [Authenticate Spotify with Multi-Scrobbler](#authenticate-spotify-with-multi-scrobbler) (outside these Redirect Instructions) go to **Step 4** and continue the instructions
Eventually, Spotify will redirect you to a web page with a URL like `http://127.0.0.1:5079/callback?code=xxxxxx...`
![echo](../echo.png)
4. In the URL, replace `127.0.0.1:5079` with the address/IP for your multi-scrobbler instance so the URL is similar to `http://192.168.0.101:9078/callback?code=xxxxxx...`. Then navigate to this URL to complete authentication.
</TabItem>
<TabItem value="msEcho" label="Echo from echo.multi-scrobbler.app">
For your convenience, I run the [`mendhak/http-https-echo:36`](https://github.com/mendhak/docker-http-https-echo) container from the [**Echo**](./?spotifyRedirect=echo) approach on a domain I, the multi-scrobbler developer [FoxxMD](https://github.com/FoxxMD), control and self-host. Please be aware:
* I **do not collect personal data** or any querystrings/data sent in requests to this site
* It's **not possible** for me to use the code sent by Spotify to this URL without also having your Client ID/Secret
* However, there is **always** risk involved with trusting a service you do not control. Evaluate the risk of using this site for yourself.
Use the [**Echo**](./?spotifyRedirect=echo) approach, starting from **Step 2**, with **https://echo.multi-scrobbler.app/callback** as Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV.
</TabItem>
</Tabs>
</details>
4. Save your new App, then copy the Client ID/Secret from the App's Basic Information page.
5. Add the Client ID, Secret, and Redirect URI to the respective field/ENV in the [configuration section below](#configuration)
6. Start multi-scrobbler, then visit the Web Dashboard and click **(Re)authenticate** on the Spotify card to start the auth process
## Configuration
<Config config="SpotifySourceConfig" fileContent={SpotifyConfig} name="spotify">
| Environmental Variable | Required? | Default | Description |
| ----------------------- | --------- | -------------------------------- | -------------------------- |
| `SPOTIFY_CLIENT_ID` | Yes | | |
| `SPOTIFY_CLIENT_SECRET` | Yes | | |
| `SPOTIFY_REDIRECT_URI` | No | `http://localhost:9078/callback` | URI must end in `callback` |
</Config>
@@ -0,0 +1,39 @@
---
title: Subsonic
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 SubsonicConfig from '!!raw-loader!@site/../config/subsonic.json.example';
This Source can be used for any application that implements the [Subsonic API](http://www.subsonic.org/pages/api.jsp) and supports the [`getNowPlaying`](http://www.subsonic.org/pages/api.jsp#getNowPlaying) endpoint (such as [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
<DetailsAdmo type="warning" summary="Known Issues">
* Multiple artists are reported as one value and cannot be separated
* If using [Airsonic Advanced](https://github.com/airsonic-advanced/airsonic-advanced) the password used (under **Credentials**) must be **Decodable**
</DetailsAdmo>
<DetailsAdmo type="tip" summary="Recommended Alternative Sources">
If your serivce supports scrobbling to **Listenbrainz** (such as Navidrome), considering using a [**Listenbrainz (Endpoint)** Source](/configuration/sources/listenbrainz-endpoint) instead of this one.
Service-specific scrobble implementations tend to be more accurate and provide more information (multiple artists) compared to the Subsonic API.
</DetailsAdmo>
Use the optional `usersAllow` property with **File** or **AIO** configuration to restrict scrobbling to a list of defined users.
## Configuration
<Config config="SubSonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------|
| `SUBSONIC_USER` | Yes | | |
| `SUBSONIC_PASSWORD` | Yes | | |
| `SUBSONIC_URL` | Yes | | Base url of your subsonic-api server |
</Config>
@@ -0,0 +1,111 @@
---
title: Teal.fm
description: Teal.fm (Source)
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/tealfm.json.example';
:::tip[Other Uses]
* To **scrobble to** an ATProto PDS, create a [teal.fm (Client)](/configuration/clients/tealfm)
:::
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
This Source monitors the teal.fm scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients.
Learn more about how teal.fm scrobbles work in the [teal.fm (Client)](/configuration/clients/tealfm) docs.
## Setup
Follow the **Setup** instructions in the [teal.fm (Client)](/configuration/clients/tealfm#setup) docs to get access to your Bluesky PDS repository and scrobble data.
## Configuration
See the [teal.fm (Client)](/configuration/clients/tealfm#setup) configuration for general setup. The main differences for **Source** configuration:
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
### Service Filtering
`serviceAllow` and `serviceDeny` are lists that can be used to filter what Plays are scrobbles from teal.fm. These correspond to the base domain of the music service where the Play is from, originally. The values in the list can be a subset of the actual service name (they do not need to be exact matches).
Only one filter can be used at a time. If `serviceAllow` is configured then `serviceDeny` will be ignored.
If a Play does not have an associated music service it can be allowed/denied using the special keyword `unknown`.
If you are unsure what services are being used for your scrobbles you can check this by:
* use [atproto.at](https://atproto.at/) to lookup your DID/handle
* View the **Raw Data** for the PDS Collection `fm.teal.alpha.feed.play`
* Check the `musicServiceBaseDomain` property for each record
<details>
<summary>Examples</summary>
Allowing only scrobbles from [plyr.fm](https://plyr.fm/):
```json title="tealfm.json"
[
{
"name": "myTealSource",
"configureAs": "source",
"data": {
"identifier": "alice.bsky.social",
"appPassword": "twog-phu7-4dhe-y4j3",
"serviceAllow": ["plyr"],
}
}
]
```
Deny scrobbles from last.fm:
```json title="tealfm.json"
[
{
"name": "myTealSource",
"configureAs": "source",
"data": {
"identifier": "alice.bsky.social",
"appPassword": "twog-phu7-4dhe-y4j3",
"serviceDeny": ["last"],
}
}
]
```
Allow scrobbles from plyr.fm and unknown services:
```json title="tealfm.json"
[
{
"name": "myTealSource",
"configureAs": "source",
"data": {
"identifier": "alice.bsky.social",
"appPassword": "twog-phu7-4dhe-y4j3",
"serviceAllow": ["plyr", "unknown"],
}
}
]
```
</details>
<Config config="TealClientConfig" fileContent={JsonConfig} name="tealfm">
| Environmental Variable | Required? | Default | Description |
| :------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| `SOURCE_TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
| `SOURCE_TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password |
</Config>
+106
View File
@@ -0,0 +1,106 @@
---
title: VLC
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/vlc.json.example';
MS communicates with [VLC](https://www.videolan.org/vlc/) using the [Web (`http`) interface module](https://wiki.videolan.org/VLC_HTTP_requests/)
#### Enable HTTP Interface
Open VLC:
* **Tools** Menu -> Preferences
* In the bottom left change **Show Settings** from `Simple` to `All`
* In the updated window select `Interface -> Main interfaces`
* In the **Extra interface modules** section enable **Web** and verify that `http` is shown in the textbox
* Select `Interface -> Main interfaces -> Lua`
* In Lua HTTP -> Password -> set a password in this box
* Click **Save** at the bottom to persist your changes
* Restart VLC
* Verify the HTTP interface is accessible by navigating to `http://localhost:8080` in your browser
If the interface is accessible, after typing in your password, VLC is now ready to be used by MS.
##### Setting VLC Interface and Port
These can be set by starting VLC from command line with specific options:
* `vlc --http-host yourHostIp:yourPort` or
* `vlc --http-port yourPort` or
* modify the configuration file for VLC by editing keys with the same values as from the command line (`http-host` and `http-port`)
#### Filename Parsing
If the file being played in VLC does not have ID3 tags/metadata information multi-scrobbler can attempt to extract artist, title, and album information from the raw filename using regular expressions. Mutli-scrobbler will only do this if there is **no other information** reported by VLC about these fields.
To extract this information use [named capture groups](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group) in your expression matching the field that should be extracted: `artist` `title` and/or `album`. MS accepts multiple expressions, the first one that matches a filename will be used.
<details>
<summary>Example</summary>
Example usage in [a file-based config:](/configuration?configType=file#configuration-types)
```json5 title="config/vlc.json" {10-15}
[
{
"name": "MyVlc",
"enable": true,
"data": {
// ...
},
"options": {
"filenamePatterns": [
// Extracts artist and title from filenames that look like:
// My Artist - My Cool title.mp4
"/^\\s*(?<artist>.+?) - (?<title>.+?)\\.\\w+$/i"
],
// logs to DEBUG when MS tries to extract data from a filename
"logFilenamePatterns": true,
}
}
]
```
</details>
#### Vlc Information Reporting
If you find that VLC is incorrectly reporting track information (in its interface) but multi-scrobbler is not parsing it correctly it may be due to the _name_ of the fields VLC is reporting. mutli-scrobbler uses the most common field name but does not cover all use cases. Before reporting an issue please turn on metadata logging and include output from your logs while it is turned on:
<details>
<summary>Metadata Logging Configuration</summary>
```json5 title="config/vlc.json" {9-9}
[
{
"name": "MyVlc",
"enable": true,
"data": {
// ...
},
"options": {
"dumpVlcMetadata": true
}
}
]
```
</details>
## Configuration
<Config config="VLCSourceConfig" fileContent={JsonConfig} name="vlc">
| Environmental Variable | Required? | Default | Description |
|------------------------|------------|------------------|-------------|
| `VLC_URL` | No | `localhost:8080` | |
| `VLC_PASSWORD` | Yes | | |
</Config>
@@ -0,0 +1,95 @@
---
title: Webscrobbler
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/webscrobbler.json.example';
After installing the [Webscrobbler extension](https://web-scrobbler.com/) open the preferences/settings for it:
* Under **Accounts**
* **Add Webhook**
* API URL: `[YourMultiScrobblerInstance]/api/webscrobbler` EX `http://localhost:9078/api/webscrobbler`
* Application name: `(whatever you want)`
Reload the extension after adding the webhook.
<DetailsAdmo type="note" summary="URL Types Supported by Webscrobbler">
* **On Firefox** - Only FQNs (domain.tld), `localhost`, and `127.0.0.1` are supported for API URL due to [firefox requiring https](https://github.com/web-scrobbler/web-scrobbler/issues/4183#issuecomment-1749222006)
* **On Chromium-based Browsers** - Any domain will work for API URL
* All other browsers are untested
</DetailsAdmo>
<DetailsAdmo type="warning" summary="Public-Facing Usage">
Multi-scrobbler is **not** designed to be publicly accessible from the internet. To use Webscrobbler outside your local network you should be accessing MS through a VPN or through a reverse proxy with some kind of authentication (though this is still not recommended).
</DetailsAdmo>
### Multiple Users
If you would like use multiple WebScrobbler sources they can be matched using a **slug** at the end of the **API URL.** This requires using [a file-based config.](/configuration?configType=file#configuration-types)
<details>
<summary>Example</summary>
```json title="webscrobbler.json"
[
{
"name": "aUserWS",
"clients": [
"client1Maloja"
],
"data": {
"slug": "usera"
}
},
{
"name": "bUserWS",
"clients": [
"client2Maloja"
],
"data": {
"slug": "userb"
}
}
]
```
* To use `aUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/usera`
* To use `bUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/userb`
:::tip
`http://localhost:9078/api/webscrobbler` is matched with the first source that _that does not have a slug defined._
:::
</details>
### Connectors Black/Whitelist
MS can be configured to only scrobble, or NOT scrobble, from some WS connectors. Use the name of the website from the [supported websites](https://web-scrobbler.com/) or from the **Connectors** tab in the extension.
:::note
This affects **only** MS's behavior and does not affect the general connector behavior you have configured within the WebScrobbler extension.
:::
## Configuration
<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 |
| WS_WHITELIST | No | | Only scrobble from these WebScrobbler Connectors. Comma-delimited list |
| WS_BLACKLIST | No | | Do not scrobble from these WebScrobbler Connectors. Comma-delimited list |
</Config>
@@ -0,0 +1,37 @@
---
title: Yamaha MusicCast
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/musiccast.json.example';
Monitor [MusicCast](https://usa.yamaha.com/products/contents/audio_visual/musiccast/index.html) device/receivers for music played on Network/USB/CD inputs.
:::tip[Scrobbling Threshold]
Musiccast may not correctly report track length. If this is the case you should reduce the "listened to" **duration** using [Scrobble Thresholds](/configuration/sources#scrobble-thresholds) so that multi-scrobbler scrobbles correctly.
:::
##### URL
If only a domain/IP:PORT is provided to multi-scrobbler it will automatically append the default Musiccast API path. If your Musiccast device is located behind a custom URL (or reverse proxy) you should explicitly provide the base path to use.
EX
| `url` | Transformed URL |
| --------------------------------------- | :---------------------------------------------- |
| `http://192.168.0.101` | `http://192.168.0.101/YamahaExtendedControl/v1` |
| `http://mydomain.com/reverseProxy/cast` | `http://mydomain.com/reverseProxy/cast` |
## Configuration
<Config config="MusicCastSourceConfig" fileContent={JsonConfig} name="musiccast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :----------------------- |
| `MCAST_URL` | Yes | | The Musiccast device URL |
</Config>
@@ -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>
@@ -0,0 +1,189 @@
---
title: Youtube 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/ytmusic.json.example';
:::warning
Communication with YT Music is **unofficial** and not supported or endorsed by Google. This means that **this integration may stop working at any time** if Google decides to change how YT Music works in the browser.
:::
:::tip[Scrobble Troubleshooting]
Due to monitoring being unofficial, listening history from YTM can be inconsistent and can cause missed scrobbles.
[**See the FAQ**](/FAQ.md#youtube-music-misses-or-duplicates-scrobbles) for a detailed explanation, how to see more details about MS's detection of tracks, and how to properly report an issue.
:::
#### Authentication {#ytm-auth}
Only one of these methods needs to be used.
<Tabs groupId="ytmAuth" queryString>
<TabItem value="cookie" label="Cookies">
Use instructions from
* https://github.com/patrickkfkan/Volumio-YouTube.js/wiki/How-to-obtain-Cookie or
* https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers
to get the **Cookie** value from a browser.
It is highly recommended to [get the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) to limit the chance the session is invalidated from normal browsing.
Add the cookie to your `ytmusic.json` config in `data` or as an ENV:
```json
{
"type": "ytmusic",
"enable": true,
"name": "MyYTM",
"data": {
"cookie": "__Secure-1PSIDTS=sidts-CjEB3EgAEvCd-......"
},
"options": {
"logAuthUpdateChanges": true,
"logDiff": true
}
}
```
If MS gives you authentication errors (session invalidated) at some point in the future follow the same instructions to get new cookies.
</TabItem>
<TabItem value="oauth" label="OAuth Client">
:::warning
As of Sept 25' OAuth clients no longer seem to have permission to access the YTM service, as reported by the upstream YTM library developer and [several other users.](https://github.com/FoxxMD/multi-scrobbler/issues/345#issuecomment-3258518769)
If you have existing, working OAuth credentials keep using them but there is no gaurantee they will continue to work.
If you are setting up the YTM Source for the first time you should first try to use [Cookies](./?ytmAuth=cookie#ytm-auth).
:::
[Based on the instructions from here...](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2479689924)
* Login to [Google Cloud console](https://console.cloud.google.com/) (create an account, if necessary)
* [Create a new project](https://console.cloud.google.com/projectcreate)
* Go to APIs and services.
* Configure the OAuth consent screen
* Use the old experience if possible
* If new is unavoidable then do not fill out any branding and under Authorized Domains you can delete the empty one (in order to save)
* Add yourself as an authorized user
* Navigate to Credentials
* Create Credentials -> choose "OAuth client ID"
* Application Type is **Web Application**
* **Name** is whatever you want, leave Authorization Javascript origins blank
* Authorized redirect URIs
* This must be **exactly** the same as what is displayed in MS! For now leave it blank so we can generate it from MS first
* Create
* In the newly created client popup save the **Client ID** and **Client Secret**, then copy them into `ytmusic.json` or appropriate ENVs:
```json
{
"type": "ytmusic",
"enable": true,
"name": "MyYTM",
"data": {
"clientId": "8910....6jqupl.apps.googleusercontent.com",
"clientSecret": "GOCSPX-WGXL6BSuQ343..."
},
"options": {
"logAuthUpdateChanges": true,
"logDiff": true
}
}
```
Now, start MS and during the YTMusic startup it will log something like this:
```
Will use custom OAuth Client:
Client ID: ...
Client Secret: ...
Redirect URI: http://localhost:9078/api/ytmusic/callback?name=MyYTM
```
If the beginning of the Redirect URI (before `api`) is EXACTLY how you would reach the MS dashboard from your browser (EX `http://localhost:9078`) then edit your google oauth client section for `Authorized redirect URIs` and add the URL MS has displayed.
If it is NOT EXACTLY the same you either need to set MS's [base url](/configuration#base-url) or you can provide your own (Custom) Redirect URI for MS to use by setting it in `ytmusic.json` or ENV.
<details>
<summary>Using a Custom Redirect URI</summary>
The three parts of the URL that must be the same:
* it must start with `api` (after domain or subdirectory IE `my.domain.tld/api...` or `whatever.tld/subDir/api...`
* it must end in `ytmusic/callback`
* It must include `name=[NameOfSource]` in the query string
Remember to add your custom URL to the `Authorized redirect URIs` section in the google oauth client!
```json
{
"type": "ytmusic",
"enable": true,
"name": "MyYTM",
"data": {
"clientId": "8910....6jqupl.apps.googleusercontent.com",
"clientSecret": "GOCSPX-WGXL6BSuQ343...",
"redirectUri": "http://my.custom.domain/api/ytmusic/callback?name=MyYTM"
},
"options": {
"logAuthUpdateChanges": true,
"logDiff": true
}
}
```
</details>
AFTER changing the Authorized redirect URIs on Google Cloud console you may need to wait a few minutes for it to take affect. Then restart MS. From the dashboard click `(Re)authenticate` on the YTmusic source card and follow the auth flow:
* On the screen about "testing" make sure you hit **Continue** (not Back To Safety)
* Make sure to select ALL scopes/permissions/grants it asks you about
* `Select what [YourAppName] can access` -> Select all
Once the flow is finished MS will get the credentials and start polling automatically. You should not need to re-authenticate again after restarting MS as it saves the credentials to the `/config` folder.
</TabItem>
<TabItem value="ytt" label="YoutubeTV">
:::warning
Using the built-in YoutubeTV authentication is unlikely to work due to [Google restricting what permissions TV clients can have](https://github.com/yt-dlp/yt-dlp/issues/11462#issuecomment-2471703090). This authentication method should not be used.
:::
To authenticate start multi-scrobbler with an empty YT Music configuration. An authentication URL/code will be logged in additon to being available from the dashboard.
```
ERROR: Sign in with the code 'CLV-KFA-BVKY' using the authentication link on the dashboard or https://www.google.com/device
```
Visit the authentication URL and enter the code that was provided (also available on the dashboard). After completing the setup flow MS will log `Auth success` and the YT Music dashboard card will display as **Idle** after refreshing. Click the **Start** link to begin monitoring.
</TabItem>
</Tabs>
## Configuration
<Config config="YTMusicSourceConfig" fileContent={JsonConfig} name="ytmusic">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|-----------------------------------------------|
| YTM_COOKIE | No | | Value for Cookie Authentication |
| YTM_CLIENT_ID | No | | Client ID for OAuth Athentication |
| YTM_CLIENT_SECRET | No | | Client Secret for OAuth Athentication |
| YTM_REDIRECT_URI | No | | A custom redirect URI for OAuth Athentication |
| YTM_LOG_DIFF | No | false | Log YTM history changes |
</Config>
-523
View File
@@ -1,523 +0,0 @@
---
sidebar_position: 4
title: Scrobble Modification
toc_max_heading_level: 4
---
Multi-scrobbler configs support the ability to modify scrobble data in an automated fashion by matching and replacing strings in **title, artists, and album** at many different times in multi-scrobbler's lifecycle.
### Why?
You may need to "clean up" data from a Source or before sending to a scrobble Client due to any number of reasons:
* ID3 tags in your music collection are dirty or have repeating garbage IE `[YourMusicSource.com] My Artist - My Title`
* A Source's service often incorrectly adds data to some field IE `My Artist - My Title (Album Version)` when the title should just be `My Title`
* An Artist you listen to often is spelled different between a Source and a Client which causes duplicate scrobbles
In any scenario where a repeating pattern can be found in the data it would be nice to be able to fix it before the data gets downstream or to help prevent duplicate scrobbling. Multi-scrobbler can help you do this.
## Overview
### Journey of a Scrobble
First, let's recap the lifecycle of a scrobble in multi-scrobbler:
**Sources** are the beginning of the journey for a **Play** (song you've listened to long enough to be scrobblable)
* A Source finds a new valid **Play**
* The Source **compares** this new Play to all the other Plays it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
* The Source **discovers** the Play, adds it to Plays it has seen already, and broadcasts the Play should be scrobbled to all Clients
Scrobble **Clients** listen for discovered Plays from Sources, then...
* A Client receives a **Play** from a Source
* 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
You'll notice there is a pattern above that looks like this:
* **Before** data is compared
* Data is **compared**
* **After** data is compared
These points, during both Source and Client processes, are when you can hook into the scrobble lifecycle and modify it.
#### TLDR
In more concrete terms this is the structure of hooks within a configuration (can be used in any **Source** or **Client**):
```json5 title="lastfm.json" {10-14}
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": {/* ... */},
"compare": {/* ... */},
"postCompare": {/* ... */}
}
}
}
]
```
##### Hook
For **Sources**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if Play was already discovered
* `postCompare` - modify Play data before sending to scrobble **Clients**
For **Clients**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if it was already scrobbled
* `postCompare` - modify Play data before scrobbling it to downstream service and adding to already seen scrobbles
:::tip
Keep in mind that modifying Scrobble/Play data earlier in the lifecycle will affect that data at all times later in the lifecycle.
For example, to modify the track so it's the same anywhere it is processed in multi-scrobbler you only need to modify it in the **Source's** `preCompare` hook because all later processes will receive the data with the modified track.
:::
### Modification Parts
Each [**hook**](#hook) (`preCompare` etc...) is an object that specifies what part of the **Play** to modify:
```json5
{
"title": [/* ... */],
"artists": [/* ... */],
"album": [/* ... */]
}
```
##### Expression
and then a **list** what pattern/replacements (expressions) to use for the modification by using either simple strings or `search-replace` objects:
```json5
[
"badTerm", // remove all instances of 'badTerm'
{
"search": "anotherBadTerm", // and also match all instances of 'anotherBadTerm'
"replace": "goodTerm" // replace with the string 'goodTerm'
}
]
```
Putting it all together:
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": {
"title": [
"badTerm",
{
"search": "badTerm",
"replace": "goodTerm"
}
]
},
}
}
}
]
```
:::note
If the value of the field (title, an artist, album) is an empty string after transforming then the field is **removed.**
:::
:::tip
Modifications can also be applied to **all Sources** or **all Clients** when using the [AIO Config](./configuration.mdx?configType=aio#configuration-types) `config.json` by setting `playTransform` in `sourceDefaults` or `clientDefaults`:
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": { // will apply playTransform to all sources
"playTransform": {
"preCompare": {
"title": [
"(Album Version)"
]
}
}
},
"sources": [/* ... */],
"clients": [/* ... */]
}
```
</details>
:::
#### Compare Hook
The `compare` [hook](#hook) is slightly different than `preCompare` and `postCompare`. It consists of an object where you define which side(s) of the comparison should be modified. It also **does not modify downstream data!** Instead, the modifications are made only for use in the comparison.
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
// ...
"options": {
"playTransform": {
"compare": {
"candidate": {/* ... */}, // modify the "new" Play being compared
"existing": {/* ... */}, // modify all "existing" Play/Scrobbles the new Play is being compared against
},
}
}
}
]
```
#### Regular Expressions
In addition to plain strings [expressions](#expression) that are matched and removed you can also use Regular Expressions. Write your regex like you normally would, but as a string, and it'll automatically be parsed:
```json5
[
"/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)' and removes it
{
"search": "/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)'
"replace": "[MySite.com]" // replace with the string '[MySite.com]'
}
]
```
The `replace` property uses javascript's [`replace()` function and so can use any special string characters.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement)
### Conditional Modification
#### "When" Condition
Top-level hooks **and** individual rules also support a `when` key for testing **if they should be run.**
The `when` key is similar to a normal [modification](#modification-parts) except:
* the keys accept a single string instead of an array
* the `when` key data is an array instead of a single object
All parts of an individual `when` clause must test true to "pass" but if **any** `when` clauses pass the hook/rule is processed. Example `when` data:
```json5
{
"when": [
{
"artist": "Elephant Gym", // both of these must match the Play object (AND)
"album": "Dreams" // both of these must match the Play object (AND)
},
// OR
{
"title": "/(Remastered)$/", // both of these must match the Play object (AND)
"album": "Various Artists" // both of these must match the Play object (AND)
}
]
}
```
More succinctly:
* All parts (`artist` `album` `title`) of a `when` are `AND` conditions
* All part-objects in the `when` array are `OR` conditions
<details>
<summary>Example of top-level hook with when condition</summary>
```json5
{
// IF the artist is Elephant Gym
// THEN Run preCompare hook ELSE skip this hook
//
// Run search-replace on album
// Run regex title remove
"sourceDefaults": {
"playTransform": {
"preCompare": {
"when": [
{
"artist": "/Elephant Gym/"
}
],
"album": [
{
"search": "Dreams",
"replace": "夢境"
}
],
"title": ["/\s\-\s滾石40\s滾石撞樂隊\s40團拚經典(.+)$/i"]
},
}
}
}
```
</details>
<details>
<summary>Example of individual rule with when condition</summary>
```json5
{
// Always run preCompare
//
// On search-replace in title...
// IF artist matches "Elephant Gym"
// THEN Run regex search-replace ELSE skip this rule
//
// Run live|remastered regex remove
"sourceDefaults": {
"playTransform": {
"preCompare": {
"title": [
{
"search": "/\\s\\-\\s滾石40\\s滾石撞樂隊\\s40團拚經典(.+)$/i",
"replace": "",
"when": [
{
"artist": "/Elephant Gym/"
}
]
},
"/(\\s\\-\\s|\\s)(feat\\.(.+)|live|remastered(.+))$/i"
],
}
}
}
}
```
</details>
#### Top-level Hook array
Top-level hooks can also be an array of hooks. This makes creating multiple scenarios for top-level `when`-gated hooks easier. All hooks in the array will be run (assuming their `when`'s pass, if they exist) and their **input will be the Play object output of the previous hook in the array.**
<details>
<summary>Example</summary>
```json5
{
"sourceDefaults": {
"playTransform": {
"preCompare": [
// first lifecycle hook of preCompare to run
{
"title": [
{
"search": "something",
"replace": "else unique"
}
]
},
// second lifecycle hook of preCompare to run
{
"title": [
{
"search": "else unique",
"replace": "very demure"
}
]
},
]
}
}
}
```
</details>
### Logging
MS can log the output of hook transformations if/when they occur. In the `playTransform` object of a Source/Client config use `log`:
* `"log": true` => Output original play + final transformed output of last hook in the array
* `"log": "all"` => Output original play + final transformed output of **each** hook in the array
```json5
{
"name": "myThing",
"data": {/*...*/},
"options": {
"playTransform": {
"preCompare": {/*...*/},
"log": true
}
}
}
```
## Examples
### Remove phrase from Title in all new Plays
Removes the phrase `(Album Version)` from the Title of a Play
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"title": [
"(Album Version)"
]
}
}
}
}
```
</details>
### Remove all parenthesized content from the end of a title
<details>
<summary>Example</summary>
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
// ...
"options": {
"playTransform": {
"compare": {
"candidate": {
"title": [
"/(\(.+\))\s*$/"
]
},
"existing": {
"title": [
"/(\(.+\))\s*$/"
]
},
},
}
}
}
]
```
</details>
### Rename misspelled artist in all new Plays
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"artists": [
{
"search": "Boz Skaggs",
"replace": "Boz Scaggs"
}
]
}
}
}
}
```
</details>
### Remove "Various Artists" albums in all new Plays
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"album": [
{
"search": "Various Artists",
"replace": ""
}
]
}
}
}
}
```
</details>
### Extract primary Artist from delimited, multi-Artist string
<details>
When the Artist string is actually a multi-artist, delimited string, this search-and-replace will replace the string with just the first artist found.
Ex
```
My Artist One / My Artist Two / Another Guy
My Artist One
```
Artists are delimited with a spaced forward slash (`/`) in the regex below. Replace the contents of the `delim` capture group with the delimiter for your use case. Some more common scenarios:
* `(?<delim>\\/)` No spaces between slash IE `My Artist One/My Artist Two/Another Guy`
* `(?<delim>\\s*\\\\\s*)` Backslash instead of forward slash IE `My Artist One \ My Artist Two \ Another Guy`
* `(?<delim>,)` Comma IE `My Artist One, My Artist Two, Another Guy`
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"artists": [
{
"search": "(.*?)(?<delim>\\s*\\/\\s*)(.*$)",
"replace": "$1"
}
]
}
}
}
}
```
</details>
</details>
@@ -0,0 +1,7 @@
{
"label": "Enhance Scrobbles",
"link": {
"type": "doc",
"id": "configuration/transforms/transforms"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,299 @@
---
title: Native Stage
description: Enhancing Scrobbles with Built-in Parsing
toc_min_heading_level: 2
toc_max_heading_level: 5
---
The **Native** [Stage](/configuration/transforms#stage) uses [built-in heuristics](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/plays/playParsing.test.ts) to try to extract Artists from Play artist/track data.
This Stage is most useful for Sources that report limited data such as:
* [Subsonic](/configuration/sources/subsonic) - Reports Artists as a single string
A non-exhaustive list of heuristics:
* Splits artists in artist string using common delimiters EX `Foo Artist, Bar Guy, Baz Band - My Cool Song`
* Does not split artists with `&` in name, if other delimiters are present
* Does not split artist name when only one delimiter is present
* Splits artists on common joiner phrases (ft. feat. vs. etc...)
* Extracts artists from Play title using joiner phrases EX `My Cool Song (feat. SomeGuy)`
A practical example of what the Native Stage will do to Play data:
This Scrobble from [Subsonic](/configuration/sources/subsonic)
```json
{
"title": "Endless Possibility (feat. Wheatus)",
"artists": ["Bowling For Soup & Punk Rock Factory"]
}
```
Will be transformed into this
```json
{
"title": "Endless Possibility",
"artists": ["Bowling For Soup", "Punk Rock Factory", "Wheatus"]
}
```
## Configuration
Available properties for [Stage Configuration](/configuration/transforms#configuring-stages):
* `delimitersExtra` - A list of string characters that should be considered delimiters for artists **in addition to** multi-scrobbler's default list (`, / \ `)
* `delimiters` - A list of string characters that should be considered delimiters for artists.
* **Replaces** all delimiters (MS not use any defaults, only what you give it)
* `artistsIgnore` - a list of strings and/or regular expressions. Any monolothic artist string that matches from the list _will not be modified._
* `artistsParseFrom` a list of the properties that should be used to try to extract artists. Can be `artists` `title` or both. Defaults to both when not provided in options ( `["artists", "title"]` )
* When `artists` is present it tries to extract additional artists from artist strings
* When `title` is present it tries to extract artists from ft. feat. vs. etc... found in the track title
* Importantly, if `artists` is _not_ present in `artistsParseFrom` then _no artists_ are used at all (only those from `title`, if present)
* `artistsParseMonolithicOnly` - boolean value, defaults to `true`. When `true` native tranformer will only attempt to extract artists if the scrobble data has _only one string for artist_ (pre-transform)
* This means that, for Sources like Spotify/Jellyfin/etc. that provide proper lists of artists in their data _and the list has more than one string_, it will not try to extract artists from their strings
<details>
<summary>Example of the behavior for `artistsParseFrom`</summary>
```
The Foos, The Bars - My Cool Track (ft. Frank)
```
Config => extracted artists
```
"artistsParseFrom": ["artists", "title"] => The Foos, The Bars, Frank
"artistsParseFrom": ["artists"] => The Foos, The Bars
"artistsParseFrom": ["title"] => Frank
"artistsParseFrom": [] =>
```
</details>
### 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-modification):
```json5
{
"type": "native",
// ...
"title": false, // will not apply any changes to Play title
"artists": {
"when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied
/* ... */
},
"album": true // will always apply changes to Play album
}
```
If a rule is not present then multi-scrobbler defaults it to `true`.
### Default Stage
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#stage).
<details>
<summary>Example</summary>
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native",
// do not include name, the default native stage will be used
}
]
}
}
}
]
```
</details>
## ENV Configuration
The default configuration shown above can also be applied using [ENV Config](/configuration?configType=env#configuration-types) for individual Sources/Clients.
Use ENV `*_TRANSFORMS=native` 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.
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](/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
* Use `SUBSONIC_TRANSFORMS=native`
<details>
<summary>Example Full Docker Deploy with ENV Configuration</summary>
Using [Jellyfin + Maloja](/quickstart#create-docker-compose-file) example from Quickstart:
```yaml
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- JELLYFIN_URL=192.168.0.110:8096
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b
- JELLYFIN_USER=MyUser
- JELLYFIN_TRANSFORMS=native # applies native Stage to preTransform of Jellyfin source
- MALOJA_URL=http://192.168.0.100:42010 # maloja receives enhanced scrobble from Jellyfin
- MALOJA_API_KEY=myApiKey
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
</details>
## Examples
### Parse only artists string using a custom delimiter
<details>
<summary>Example</summary>
Your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "native",
"name": "MyNativeTransformer",
"defaults": {
// default delimiters when this Stage is used in a hook
"delimiters": [
"•"
],
// default delimiters when this Stage is used in a hook
"artistsParseFrom": ["artists"]
}
}
]
}
```
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native",
"name": "MyNativeTransformer"
}
]
}
}
}
]
```
</details>
### Don't parse specific artists
<details>
<summary>Example</summary>
Your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "native",
"name": "MyNativeTransformer",
"defaults": {
"artistsIgnore": [
"Crosby, Stills, Nash & Young",
"Polo & Pan"
]
}
}
]
}
```
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native",
"name": "MyNativeTransformer"
}
]
}
}
}
]
```
</details>
### Extract Artists from Title but do not modify Title
In the scenario where you want to extract Artists from Track titles with joiners like `My Cool Track (feat. Foobar)` but want to keep the title as-is:
<details>
<summary>Example</summary>
No additional [stage configuration](#configuration) is required so we can use the [default Stage](#default-stage).
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native",
"title": false // do not modify title
}
]
}
}
}
]
```
</details>
@@ -0,0 +1,635 @@
---
sidebar_position: 4
title: Enhancing Scrobbles
toc_max_heading_level: 5
---
Multi-scrobbler configs support the ability to enhance scrobble data in an automated fashion by matching and replacing strings in **title, artists, and album** at many different times in multi-scrobbler's lifecycle.
<details>
<summary>Why Would I Do This?</summary>
You may need to "clean up" data from a Source or before sending to a scrobble Client due to any number of reasons:
* ID3 tags in your music collection are dirty or have repeating garbage IE `[YourMusicSource.com] My Artist - My Title`
* A Source's service often incorrectly adds data to some field IE `My Artist - My Title (Album Version)` when the title should just be `My Title`
* An Artist you listen to often is spelled different between a Source and a Client which causes duplicate scrobbles
In any scenario where a repeating pattern can be found in the data it would be nice to be able to fix it before the data gets downstream or to help prevent duplicate scrobbling. Multi-scrobbler can help you do this.
</details>
## Journey of a Scrobble
First, let's recap the lifecycle of a scrobble in multi-scrobbler:
**Sources** are the beginning of the journey for a **Play** (song you've listened to long enough to be scrobblable)
* A Source finds a new valid **Play**
* The Source **compares** this new Play to all the other Plays it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
* The Source **discovers** the Play, adds it to Plays it has seen already, and broadcasts the Play should be scrobbled to all Clients
Scrobble **Clients** listen for discovered Plays from Sources, then...
* A Client receives a **Play** from a Source
* 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
## Lifecycle Hooks
You'll notice there is a pattern above that looks like this:
* **Before** data is compared
* Data is **compared**
* **After** data is compared
These points, during both Source and Client processes, are when you can hook into the scrobble lifecycle and modify it.
##### TLDR
In more concrete terms this is the structure of hooks within a configuration (can be used in any **Source** or **Client**):
```json5 title="lastfm.json" {10-14}
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": [/* ... */],
"compare": [/* ... */],
"postCompare": [/* ... */]
}
}
}
]
```
### Hook
For **Sources**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if Play was already discovered
* `postCompare` - modify Play data before sending to scrobble **Clients**
For **Clients**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if it was already scrobbled
* `postCompare` - modify Play data before scrobbling it to downstream service and adding to already seen scrobbles
<DetailsAdmo type="tip" summary="Transformed Scrobbles Are Permanently Modified">
Keep in mind that modifying Scrobble/Play data earlier in the lifecycle will affect that data at all times later in the lifecycle (except when using the **compare** hook).
For example, to modify the track so it's the same anywhere it is processed in multi-scrobbler you only need to modify it in the **Source's** `preCompare` hook because all later processes will receive the data with the modified track.
</DetailsAdmo>
:::tip[TLDR]
**To modify a scrobble coming from one of your Sources use the `preCompare` hook.**
:::
<details>
<summary>Using `compare` hook</summary>
The `compare` [hook](#hook) is slightly different than `preCompare` and `postCompare`. It consists of an object where you define which side(s) of the comparison should be modified. It also **does not modify downstream data!** Instead, the modifications are made only for use in the comparison.
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
// ...
"options": {
"playTransform": {
"compare": [
{
"candidate": {/* ... */}, // modify the "new" Play being compared
"existing": {/* ... */}, // modify all "existing" Play/Scrobbles the new Play is being compared against
}
],
}
}
}
]
```
</details>
## Modification Stage {#stage}
Each [**hook**](#hook) is made up of one or more **Stages**. A Stage is a self-contained, unique way of enhancing or modifying the Play data. Some examples of a Stage:
* The [User](/configuration/transforms/user) Stage allows a user to define search-and-replace terms for Artist/Title/Album
* The [Native](/configuration/transforms/native) Stage uses MS's built-in heuristics to extract Artists from a single Artist string
* The [Musicbrainz](/configuration/transforms/musicbrainz) Stage tries to match Play data with the Musicbrainz database and to standardize the Artist/Title/Album data
Each Stage in a Hook receives Play data from the previous Stage.
Within a hook, each Stage minimally consists of a `type` to identify what Stage it is along with any other data specific to that stage:
```json5
{
"type": "native"
// optional, stage specific data here...
}
```
### Configuring Stages {#configuring-stages}
Stages may be globally configured using [AIO Config](/configuration?configType=aio#configuration-types) `config.json` file in the top-level `transformers` block.
Each Stage consists of:
* `type` the type of Stage
* `name` a unique name for the Stage, to be (potentially) used with hooks
* `defaults` - An object defining default configuration for this stage, when used in a Hook.
* `data` - An object containing any data required to initially configure the stage itself (Example: API URL, username, password, etc...)
<details>
<summary>Example</summary>
Your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "native",
"name": "MyNativeTransformer",
"defaults": {
// default delimiters when this Stage is used in a hook
"delimiters": [
"•"
],
// default delimiters when this Stage is used in a hook
"artistsParseFrom": ["artists"]
}
}
]
}
```
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native"
"name": "MyNativeTransformer" // when "name" is not defined, uses first found "native" transformer
}
]
}
}
}
]
```
</details>
Multiple stages of the same type may be configured, allowing you to define several sets of default behavior.
<details>
<summary>Example</summary>
Your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "native",
"name": "DotTransformer",
"defaults": {
"delimiters": [
"•"
],
"artistsParseFrom": ["artists"]
}
},
{
"type": "native",
"name": "TitleOnly",
"defaults": {
// extracts and uses *only* artists found in title string
"artistsParseFrom": ["title"]
}
}
]
}
```
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native"
"name": "DotTransformer"
}
]
}
}
}
]
```
In a [VLC](/configuration/sources/vlc) [File Config](/configuration?configType=file#configuration-types):
```json5 title="vlc.json"
[
{
"name": "MyVLC",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native"
"name": "TitleOnly"
}
]
}
}
}
]
```
</details>
#### Overriding Configuration
The default configuration you set for your Stage may be overridden in any usage of the Stage within a Hook.
<details>
<summary>Example</summary>
Your [AIO Config](/configuration?configType=aio#configuration-types):
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "native",
"name": "MyNativeTransformer",
"defaults": {
// default delimiters when this Stage is used in a hook
"delimiters": [
"•"
],
// default delimiters when this Stage is used in a hook
"artistsParseFrom": ["artists"]
}
}
]
}
```
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "native"
"name": "MyNativeTransformer",
// overrides property from "defaults"
"artistsParseFrom": ["artists", "title"]
}
]
}
}
}
]
```
</details>
### Rules for Play Data {#stage-rules}
Each [Stage](#stage) may specify whether it should apply the resulting transformation to different parts of the Play data by specifying `title`, `artists` and/or `album` in the Stage object.
```json5
{
"type": "native",
// ...
"title": false, // will not apply any changes to Play title
"artists": {
"when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied
/* ... */
},
"albumArtists": true, // will always apply changes to Play album artists
"album": true, // will always apply changes to Play album
"duration": true, // will always apply changes to Play duration (length of track)
"meta": true, // will always apply changes to Play meta (MBIDs, spotify links, etc...)
}
```
The actual value of each property may be different for each Stage. Check the docs for the Stage you want to use to see its usage of each.
Generically, though, each property may be some value **or** an object combining a [`when` condition](#conditional-modification) and that value.
**If none of the properties are specified in the stage then it's assumed all transformed data should be used.**
:::note
Specifying these Rules is **not** the same as [configuring the Stage](#configuring-stages). Rules only determine if the *result* of the transformation should be used (replace) the existing Play Data.
:::
## Stage Flow Control {#flow-control}
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](#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
* `onSkip` (default `continue`) - If the Stage does not process the Play data due to stage-level [`when`](#conditional-modification) or other stage-specific skip conditions
:::tip
The default behaviors for Flow Control are the same as you would intuitively think Stages should run IE the next Stage you have defined runs if the current Stage does not fail.
Specifying Flow Control is only necessary if the above assumptions are not true for your scenario.
<details>
<summary>Default Flow Control Example</summary>
```json5 title="subsonic.json"
// ...
"options": {
"playTransform": {
"preCompare": [
{
// if this stage is successful, native is run
// if this stage fails, native does not run
"type": "musicbrainz",
"name": "MyMB"
},
{
"type": "native",
"name": "MyNative"
}
]
}
}
```
</details>
:::
<details>
<summary>Customized Flow Control Example</summary>
```json5 title="subsonic.json"
// ...
"options": {
"playTransform": {
"preCompare": [
{
// if musicbrainz is successful then do NOT run native,
// only run native if musicbrainz fails to find a match (onFailure)
"type": "musicbrainz",
"name": "MyMB"
"onSuccess": "stop",
"onFailure": "continue"
},
{
"type": "native",
"name": "MyNative"
}
]
}
}
```
</details>
### Conditionally Run {#conditional-modification}
[Stages](#stage) within a [Hook](#hook), and [Rules](#stage-rules) within each Stage, support a `when` object for testing **if they should be run.**
The `when` object may have propertes for rule. Each property may be a string or regular expression. The value of the property is used to match the **pre-transformation** values from Play data.
All parts of an individual `when` clause must test true to "pass" but if **any** `when` clauses pass the Stage/Rule is processed.
If the `when` test does not pass then the Stage is [**skipped (`onSkip`)**](#flow-control).
```json5
{
"when":
{
"artist": "Elephant Gym", // Play must have an artist matching "Elephant Gym" (AND)
"album": "Dreams" // Play object must have an album matching "Dreams" (AND)
}
}
```
```json5
{
"when": [
{
"artist": "Elephant Gym", // Play must have an artist matching "Elephant Gym" (AND)
"album": "Dreams" // Play object must have an album matching "Dreams" (AND)
},
// OR
{
"title": "/(Remastered)$/", // Play title must match regular expression (AND)
"album": "Various Artists" // Play album must match "Various Artists" (AND)
}
]
}
```
More succinctly:
* All parts (`artist` `album` `albumArtist` `duration` `meta` `title`) of a `when` are `AND` conditions
* All part-objects in the `when` array are `OR` conditions
<details>
<summary>Example of Stage with `when` condition</summary>
```json5
{
// IF the artist is Elephant Gym
// THEN Run native stage
"playTransform": {
"preCompare": [
{
"type": "native",
"when": [
{
"artist": "/Elephant Gym/"
}
]
}
],
}
}
```
</details>
<details>
<summary>Example of individual rule with when condition</summary>
```json5
{
// Always run native Stage
//
// IF artist matches "Elephant Gym"
// THEN use result of native stage for "artists" of Play data
"playTransform": {
"preCompare": {
"type": "native",
"artists":
{
"when": [
{
"artist": "/Elephant Gym/"
}
]
},
}
}
}
```
</details>
## Logging
MS can log a diff of Stage transformations if/when they occur. In the `playTransform` object of a Source/Client config use `log`:
* `"log": true` => Output original play + final transformed output of last Stage in the array
* This can also be enabled by using [`DEBUG_MODE=true`](/configuration/#debug-mode)
* `"log": "all"` => Output original play + final transformed output of **each** Stage in the array
```json5
{
"name": "myThing",
"data": {/*...*/},
"options": {
"playTransform": {
"preCompare": {/*...*/},
"log": true
}
}
}
```
<details>
<summary>Example</summary>
The output shows the diff between the previous stage (or original Play) and the current stage. In docker logs this is highlighted with diff syntax:
```diff
[2025-12-17 08:53:10.467 -0500] DEBUG : [App] [Sources] [Lastfm - mylfm] [Play Transform] [preCompare] [VLDJJo] Transform Diff
- Original
+ musicbrainz-MyMB
Title : Demons Theme Part II (original 12" mix)
Artists : LTJ Bukem
Album Artists: (None)
- Album : Producer 05: Rarities (Original 12" Version)
+ Album : Producer 05: Rarities (original 12" version)
Meta :
* brainz-album: 36759a8a-d3df-47da-a236-60f84fdc0cab
+ * brainz-artist: 28c1b7b7-355a-48b1-b2c4-75b8eb8080ef
+ * brainz-track: a0d51240-7ef1-4676-882e-be9f354075cb
+ * brainz-releaseGroup: e0cce74f-abcc-343b-9390-1673e4d57ce7
```
</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):
* [User Stage](/configuration/transforms/user#examples)
* [Native Stage](/configuration/transforms/native#examples)
* [Musicbrainz Stage](/configuration/transforms/musicbrainz#examples)
@@ -0,0 +1,312 @@
---
title: User Stage
description: Enhancing Scrobbles with Regex Parsing
toc_min_heading_level: 2
toc_max_heading_level: 5
---
The **User** [Stage](/configuration/transforms#stage) uses [**search-and-replace** expressions](#search-and-replace-expression), provided by you, to modify/replace parts of Play data.
This Stage is most useful for correcting individual instances of bad data, or patterns, in your known data. Example scenarios:
* Removing `(Album Version)` from all Titles
* Removing `Various Artists` from Artist data
* Correcting spelling mistakes for individual Artist names
The user [Stage `type`](/configuration/transforms#stage) is `user`.
## Configuration
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>
<summary>Example</summary>
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "user",
"name": "Normalizer",
"defaults": {
"title": [
"(Album Version)"
],
"artists": [
"Various Artists"
]
}
}
]
}
```
</details>
#### Default Stage
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#stage).
<details>
<summary>Example</summary>
In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?configType=file#configuration-types):
```json5 title="subsonic.json"
[
{
"name": "MySubsonic",
"data": { /* ... */},
"options": {
"playTransform": {
"preCompare": [
{
"type": "user",
// do not include name, the default user stage will be used
"title": [
// removes badterm from track title
"badterm"
]
}
]
}
}
}
]
```
</details>
### Search-And-Replace Expression
A Search-And-Replace Expression can be a plain string that matches a literal, then removes it:
```
Expression: "badTerm"
"this is badTerm cool string" => "this is a cool string"
```
or a regular expression that matches and removes the match:
```
Expression: "/bad\w+/i"
"this is badSomething cool string" => "this is a cool string"
```
Or it may be an object that specifies what to match (using either plain string or regular expression) and what to replace it with:
```json5
{
"search": "anotherBadTerm", // match all instances of 'anotherBadTerm'
"replace": "goodTerm" // replace with the string 'goodTerm'
}
```
```
"this is anotherBadTerm cool string" => "this is goodTerm cool string"
```
```json5
{
"search": "/^\(\w+.com)/i", // matches any string that starts with EX '(YourMusic.com)'
"replace": "[MySite.com]" // replace with the string '[MySite.com]'
}
```
```
"(Foo.com) this is a cool string" => "[MySite.com] this is a cool string"
```
The `replace` property uses javascript's [`replace()` function and so can use any special string characters.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement)
### Rules
Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-and-replace expressions](#search-and-replace-expression):
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"type": "user",
"preCompare": [
{
"title": [
// removes "badTerm" from title
"badTerm",
{
// removes "fooTerm" from title and replaces it with "barTerm"
"search": "fooTerm",
"replace": "barTerm"
}
]
}
],
}
}
}
]
```
:::note
If the value of the field (title, an artist, album) is an empty string after transforming then the field is **removed.**
:::
## Examples
### Usage with `when` condition
Using `when` for [Conditional Modification](/configuration/transforms#conditional-modification)
<details>
<summary>Example</summary>
```json5
// On search-replace in title...
// IF artist matches "Elephant Gym"
// THEN Run regex search-replace ELSE skip this rule
//
// Run live|remastered regex remove on title
{
"title": [
{
"search": "/\\s\\-\\s滾石40\\s滾石撞樂隊\\s40團拚經典(.+)$/i",
"replace": "",
"when": [
{
"artist": "/Elephant Gym/"
}
]
},
"/(\\s\\-\\s|\\s)(feat\\.(.+)|live|remastered(.+))$/i"
],
}
```
</details>
### Remove phrase from Title in all new Plays
Removes the phrase `(Album Version)` from the Title of a Play
<details>
<summary>Example</summary>
```json5
{
"title": [
"(Album Version)"
]
}
```
</details>
### Remove all parenthesized content from the end of a title
<details>
<summary>Example</summary>
```json5
{
"compare": {
"candidate": {
"title": [
"/(\(.+\))\s*$/"
]
},
"existing": {
"title": [
"/(\(.+\))\s*$/"
]
},
},
}
```
</details>
### Rename misspelled artist in all new Plays
<details>
<summary>Example</summary>
```json5
{
"artists": [
{
"search": "Boz Skaggs",
"replace": "Boz Scaggs"
}
]
}
```
</details>
### Remove "Various Artists" albums in all new Plays
<details>
<summary>Example</summary>
```json5
{
"album": [
{
"search": "Various Artists",
"replace": ""
}
]
}
```
</details>
### Extract primary Artist from delimited, multi-Artist string
<details>
When the Artist string is actually a multi-artist, delimited string, this search-and-replace will replace the string with just the first artist found.
Ex
```
My Artist One / My Artist Two / Another Guy
My Artist One
```
Artists are delimited with a spaced forward slash (`/`) in the regex below. Replace the contents of the `delim` capture group with the delimiter for your use case. Some more common scenarios:
* `(?<delim>\\/)` No spaces between slash IE `My Artist One/My Artist Two/Another Guy`
* `(?<delim>\\s*\\\\\s*)` Backslash instead of forward slash IE `My Artist One \ My Artist Two \ Another Guy`
* `(?<delim>,)` Comma IE `My Artist One, My Artist Two, Another Guy`
<details>
<summary>Example</summary>
```json
{
"artists": [
{
"search": "(.*?)(?<delim>\\s*\\/\\s*)(.*$)",
"replace": "$1"
}
]
}
```
</details>
</details>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Development",
"position": 5,
"position": 6,
"link": {
"type": "generated-index",
"description": "Developing for Multi-Scrobbler and tutorials"
-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...
+14 -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,4 +177,16 @@ 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
Run tsx with inspect args
```
NODE_ENV=production node node_modules/.bin/tsx --inspect --heap-prof src/backend/index.ts
```
Use `chrome://inspect` from a chromium-based browser and attach to the running process, usually `localhost:9229`
From the opened DevTools window use Performance or Memory to profile the running process.

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