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

Configure Feed

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

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.

FoxxMD (Jan 19, 2026, 3:45 PM UTC) 9ccdd53b 8e6730a5

+186 -44
+3 -3
docsite/docs/configuration/clients/koito.mdx
··· 54 54 <Config config="KoitoClientConfig" fileContent={JsonConfig} client name="koito"> 55 55 | Environmental Variable | Required? | Default | Description | 56 56 | :--------------------- | --------- | ------- | --------------------------------- | 57 - | KOITO_TOKEN | Yes | | API Key from your Koito Account | 58 - | KOITO_USER | Yes | | Your Koito username | 59 - | KOITO_URL | No | | The base URL for the Koito server | 57 + | `KOITO_TOKEN` | Yes | | API Key from your Koito Account | 58 + | `KOITO_USER` | Yes | | Your Koito username | 59 + | `KOITO_URL` | No | | The base URL for the Koito server | 60 60 </Config>
+5 -5
docsite/docs/configuration/clients/listenbrainz.mdx
··· 23 23 ## Configuration 24 24 25 25 <Config config="ListenbrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz"> 26 - | Environmental Variable | Required? | Default | Description | 27 - |------------------------|-----------|-------------------------------|---------------------------------| 28 - | LZ_TOKEN | Yes | | User token from your LZ profile | 29 - | LZ_USER | Yes | | Your LZ username | 30 - | LZ_URL | No | https://api.listenbrainz.org/ | The base URL for the LZ server | 26 + | Environmental Variable | Required? | Default | Description | 27 + | ---------------------- | --------- | ----------------------------- | ------------------------------- | 28 + | `LZ_TOKEN` | Yes | | User token from your LZ profile | 29 + | `LZ_USER` | Yes | | Your LZ username | 30 + | `LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server | 31 31 </Config>
+2 -2
docsite/docs/configuration/clients/rocksky.mdx
··· 67 67 <Config config="RockskyClientConfig" fileContent={JsonConfig} client name="rocksky"> 68 68 | Environmental Variable | Required? | Default | Description | 69 69 | :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- | 70 - | ROCKSKY_HANDLE | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` | 71 - | ROCKSKY_KEY | Yes | | API Key obtained from Rocksky developer dashboard | 70 + | `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` | 71 + | `ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard | 72 72 </Config>
+4 -4
docsite/docs/configuration/clients/tealfm.mdx
··· 71 71 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. 72 72 73 73 <Config config="TealClientConfig" fileContent={JsonConfig} client name="tealfm"> 74 - | Environmental Variable | Required? | Default | Description | 75 - | :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- | 76 - | TEALFM_IDENTIFIER | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` | 77 - | TEALFM_APP_PW | Yes | | Bluesky/ATProto network App Password | 74 + | Environmental Variable | Required? | Default | Description | 75 + | :--------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ | 76 + | `TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` | 77 + | `TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password | 78 78 </Config>
+5 -3
docsite/docs/configuration/sources/koito-source.mdx
··· 25 25 #### Configuration 26 26 27 27 <Config config="KoitoSourceConfig" fileContent={KoitoConfig} name="koito"> 28 - :::note 29 - You cannot use ENV variables shown in the [Koito Client config](/configuration/clients/koito) -- multi-scrobbler assumes Koito ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Koito as a Source. 30 - ::: 28 + | Environmental Variable | Required? | Default | Description | 29 + | :--------------------- | --------- | ------- | --------------------------------- | 30 + | `SOURCE_KOITO_TOKEN` | Yes | | API Key from your Koito Account | 31 + | `SOURCE_KOITO_USER` | Yes | | Your Koito username | 32 + | `SOURCE_KOITO_URL` | No | | The base URL for the Koito server | 31 33 </Config>
+6 -3
docsite/docs/configuration/sources/lastfm-source.mdx
··· 23 23 ## Configuration 24 24 25 25 <Config config="LastfmSourceConfig" fileContent={LastfmConfig} name="lastfm"> 26 - :::note 27 - You cannot use ENV variables shown in the [Last.fm Client config](/configuration/clients/lastfm) -- multi-scrobbler assumes Last.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Last.fm as a Source. 28 - ::: 26 + | Environmental Variable | Required? | Default | Description | 27 + | ---------------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- | 28 + | `SOURCE_LASTFM_API_KEY` | Yes | | Api Key from your API Account | 29 + | `SOURCE_LASTFM_SECRET` | Yes | | Shared secret from your API Account | 30 + | `SOURCE_LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it | 31 + | `SOURCE_LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. | 29 32 </Config>
+8 -3
docsite/docs/configuration/sources/librefm-source.mdx
··· 22 22 ## Configuration 23 23 24 24 <Config config="LibrefmSourceConfig" fileContent={JsonConfig} name="librefm"> 25 - :::note 26 - You cannot use ENV variables shown in the [Libre.fm Client config](/configuration/clients/librefm) -- multi-scrobbler assumes Libre.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Libre.fm as a Source. 27 - ::: 25 + | Environmental Variable | Required? | Default | Description | 26 + | ----------------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ | 27 + | `SOURCE_LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables | 28 + | `SOURCE_LIBREFM_API_KEY` | No | `anything` | Optional API Key | 29 + | `SOURCE_LIBREFM_SECRET` | No | `anything` | Optional shared secret | 30 + | `SOURCE_LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it | 31 + | `SOURCE_LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. | 32 + | `SOURCE_LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` | 28 33 </Config>
+5 -3
docsite/docs/configuration/sources/listenbrainz-source.mdx
··· 25 25 ## Configuration 26 26 27 27 <Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} name="listenbrainz"> 28 - :::note 29 - You cannot use ENV variables shown in the [Listenbrainz Client config](/configuration/clients/listenbrainz) -- multi-scrobbler assumes Listenbrainz ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Listenbrainz as a Source. 30 - ::: 28 + | Environmental Variable | Required? | Default | Description | 29 + | ---------------------- | --------- | ----------------------------- | ------------------------------- | 30 + | `SOURCE_LZ_TOKEN` | Yes | | User token from your LZ profile | 31 + | `SOURCE_LZ_USER` | Yes | | Your LZ username | 32 + | `SOURCE_LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server | 31 33 </Config>
+4 -3
docsite/docs/configuration/sources/maloja.mdx
··· 23 23 ## Configuration 24 24 25 25 <Config config="MalojaSourceConfig" fileContent={MalojaConfig} name="maloja"> 26 - :::note 27 - You cannot use ENV variables shown in the [Maloja Client config](/configuration/clients/maloja) -- multi-scrobbler assumes Maloja ENVs are always used for the **client** configuration. You must use the [[File or AIO](/configuration?configType=file#configuration-types) config to setup Maloja as a Source. 28 - ::: 26 + | Environmental Variable | Required? | Default | Description | 27 + | ----------------------- | --------- | ------- | ----------------------------- | 28 + | `SOURCE_MALOJA_URL` | Yes | | Base URL of your installation | 29 + | `SOURCE_MALOJA_API_KEY` | Yes | | Api Key | 29 30 </Config>
+4 -3
docsite/docs/configuration/sources/rocksky-source.mdx
··· 33 33 * [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"` 34 34 35 35 <Config config="RockskyClientConfig" fileContent={JsonConfig} name="rocksky"> 36 - :::note 37 - You cannot use ENV variables shown in the [Rocksky Client config](/configuration/clients/rocksky#configuration) -- multi-scrobbler assumes Rocksky ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Rocksky as a Source. 38 - ::: 36 + | Environmental Variable | Required? | Default | Description | 37 + | :---------------------- | --------- | ------- | ----------------------------------------------------------------------------------- | 38 + | `SOURCE_ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` | 39 + | `SOURCE_ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard | 39 40 </Config>
+4 -3
docsite/docs/configuration/sources/tealfm-source.mdx
··· 104 104 </details> 105 105 106 106 <Config config="TealClientConfig" fileContent={JsonConfig} name="tealfm"> 107 - :::note 108 - You cannot use ENV variables shown in the [teal.fm Client config](/configuration/clients/tealfm#configuration) -- multi-scrobbler assumes teal.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup teal.fm as a Source. 109 - ::: 107 + | Environmental Variable | Required? | Default | Description | 108 + | :------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ | 109 + | `SOURCE_TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` | 110 + | `SOURCE_TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password | 110 111 </Config>
+1 -4
src/backend/scrobblers/ScrobbleClients.ts
··· 214 214 apiKey: process.env.LASTFM_API_KEY, 215 215 secret: process.env.LASTFM_SECRET, 216 216 redirectUri: process.env.LASTFM_REDIRECT_URI, 217 - session: process.env.LASTFM_SESSION, 218 - librefm: process.env.LASTFM_LIBREFM_MODE !== undefined ? parseBool(process.env.LASTFM_LIBREFM_MODE, undefined) : undefined, 219 - host: process.env.LASTFM_HOST, 220 - path: process.env. LASTFM_PORT 217 + session: process.env.LASTFM_SESSION 221 218 }; 222 219 if (!Object.values(lfm).every(x => x === undefined)) { 223 220 configs.push({
+135 -5
src/backend/sources/ScrobbleSources.ts
··· 410 410 } 411 411 break; 412 412 case 'lastfm': 413 - // sane default for lastfm is that user want to scrobble TO it, not FROM it -- this is also existing behavior 414 - defaultConfigureAs = 'client'; 413 + { 414 + const lfm = { 415 + apiKey: process.env.SOURCE_LASTFM_API_KEY, 416 + secret: process.env.SOURCE_LASTFM_SECRET, 417 + redirectUri: process.env.SOURCE_LASTFM_REDIRECT_URI, 418 + session: process.env.SOURCE_LASTFM_SESSION, 419 + }; 420 + if (!Object.values(lfm).every(x => x === undefined)) { 421 + configs.push({ 422 + type: 'lastfm', 423 + name: 'unnamed-lfm-source', 424 + source: 'ENV', 425 + mode: 'single', 426 + configureAs: 'source', 427 + data: lfm, 428 + options: transformPresetEnv('SOURCE_LASTFM') 429 + }) 430 + } 431 + } 415 432 break; 416 433 case 'deezer': 417 434 const d = { ··· 451 468 }); 452 469 } 453 470 break; 454 - case 'listenbrainz': 455 - // sane default for lastfm is that user want to scrobble TO it, not FROM it -- this is also existing behavior 456 - defaultConfigureAs = 'client'; 471 + case 'maloja': { 472 + // env builder for single user mode 473 + const url = process.env.SOURCE_MALOJA_URL; 474 + const apiKey = process.env.SOURCE_MALOJA_API_KEY; 475 + if (url !== undefined || apiKey !== undefined) { 476 + configs.push({ 477 + type: 'maloja', 478 + name: 'unnamed-mlj-source', 479 + source: 'ENV', 480 + mode: 'single', 481 + configureAs: 'source', 482 + data: { 483 + url, 484 + apiKey 485 + }, 486 + options: transformPresetEnv('SOURCE_MALOJA') 487 + }) 488 + } 489 + } 490 + break; 491 + case 'librefm':{ 492 + const shouldUse = parseBool(process.env.LIBRFM_ENABLE) 493 + const libre = { 494 + apiKey: process.env.SOURCE_LIBREFM_API_KEY, 495 + secret: process.env.SOURCE_LIBREFM_SECRET, 496 + redirectUri: process.env.SOURCE_LIBREFM_REDIRECT_URI, 497 + session: process.env.SOURCE_LIBREFM_SESSION, 498 + urlBase: process.env.SOURCE_LIBREFM_URLBASE, 499 + }; 500 + if (!Object.values(libre).every(x => x === undefined) || shouldUse) { 501 + configs.push({ 502 + type: 'librefm', 503 + name: 'unnamed-librefm-source', 504 + source: 'ENV', 505 + mode: 'single', 506 + configureAs: 'source', 507 + data: libre, 508 + options: transformPresetEnv('SOURCE_LIBREFM') 509 + }) 510 + } 511 + } 512 + break; 513 + case 'listenbrainz': { 514 + const lz = { 515 + url: process.env.SOURCE_LZ_URL, 516 + token: process.env.SOURCE_LZ_TOKEN, 517 + username: process.env.SOURCE_LZ_USER 518 + }; 519 + if (!Object.values(lz).every(x => x === undefined)) { 520 + configs.push({ 521 + type: 'listenbrainz', 522 + name: 'unnamed-lz-source', 523 + source: 'ENV', 524 + mode: 'single', 525 + configureAs: 'source', 526 + data: lz, 527 + options: transformPresetEnv('SOURCE_LZ') 528 + }) 529 + } 530 + } 531 + break; 532 + case 'koito': { 533 + const koit = { 534 + url: process.env.SOURCE_KOITO_URL, 535 + token: process.env.SOURCE_KOITO_TOKEN, 536 + username: process.env.SOURCE_KOITO_USER 537 + }; 538 + if (!Object.values(koit).every(x => x === undefined)) { 539 + configs.push({ 540 + type: 'koito', 541 + name: 'unnamed-koito-source', 542 + source: 'ENV', 543 + mode: 'single', 544 + configureAs: 'source', 545 + data: koit, 546 + options: transformPresetEnv('SOURCE_KOITO') 547 + }) 548 + } 549 + } 550 + break; 551 + case 'tealfm': { 552 + const teal = { 553 + identifier: process.env.SOURCE_TEALFM_IDENTIFIER, 554 + appPassword: process.env.SOURCE_TEALFM_APP_PW, 555 + pds: process.env.SOURCE_TEALFM_PDS 556 + }; 557 + if (!Object.values(teal).every(x => x === undefined)) { 558 + configs.push({ 559 + type: 'tealfm', 560 + name: 'unnamed-tealfm-source', 561 + source: 'ENV', 562 + mode: 'single', 563 + configureAs: 'source', 564 + data: teal, 565 + options: transformPresetEnv('SOURCE_TEALFM') 566 + }) 567 + } 568 + } 569 + break; 570 + case 'rocksky': { 571 + const rocksky = { 572 + key: process.env.SOURCE_ROCKSKY_KEY, 573 + handle: process.env.SOURCE_ROCKSKY_HANDLE 574 + }; 575 + if (!Object.values(rocksky).every(x => x === undefined)) { 576 + configs.push({ 577 + type: 'rocksky', 578 + name: 'unnamed-rocksky-source', 579 + source: 'ENV', 580 + mode: 'single', 581 + configureAs: 'source', 582 + data: rocksky, 583 + options: transformPresetEnv('SOURCE_ROCKSKY') 584 + }) 585 + } 586 + } 457 587 break; 458 588 case 'endpointlz': 459 589 const lzShouldUse = parseBool(process.env.LZENDPOINT_ENABLE);