Compare commits

...
128 Commits
Author SHA1 Message Date
FoxxMD 335880249f chore(release): Bump version for release 2026-08-07 16:49:36 +00:00
Matt Foxx 5c2917f906 Merge pull request #660 from FoxxMD/ingressSot
Ingress sot
2026-08-07 12:34:15 -04:00
FoxxMD 124e857d9f fix: now playing playerUpdate event data 2026-08-07 14:53:02 +00:00
FoxxMD 5dee499381 test: fix failing tests from np data structure change 2026-08-07 14:53:02 +00:00
FoxxMD 10325fd4e6 refactor(now playing): Use more efficient solution for ingress now playing
Re-queue valid ingress NP update on client rather than spamming all clients with duplicate updates from memory source

This should have the same functional affect with way less log noise
2026-08-07 14:53:02 +00:00
FoxxMD 045890e243 feat(sourc): Add Ingress SOT type and force-push ingress player updates
#659
2026-08-07 14:53:02 +00:00
FoxxMD 685e48a7a6 fear(ui): Implement cache clearing mechanism #634 2026-08-06 20:20:11 +00:00
FoxxMD 7eb5104f0b feat(ui): Implement basic settings page 2026-08-06 19:34:02 +00:00
FoxxMD 4272e8ea58 feat(ui): Improve UX for theme switcher
* Remove three-state toggle
* Revert to system theme only when *user* initiated toggle results in same theme as system
2026-08-06 18:24:57 +00:00
FoxxMD 8e6d35a85b fix(lz endpoint): Fix missing early return when no sources for playing now response 2026-08-05 19:08:08 +00:00
FoxxMD 917d05f295 docs: Update configuration types language 2026-08-05 15:02:21 +00:00
FoxxMD 287c994643 docs: Some corrections and updated language 2026-08-05 14:15:25 +00:00
Matt Foxx 7980a0c856 Merge pull request #657 from FoxxMD/lastfmEndpointResponses
feat(lfm endpoint): Implement more api emulation
2026-08-05 09:28:43 -04:00
FoxxMD 9d2557cf6a feat(lfm endpoint): Handle scrobble payload variants 2026-08-04 22:14:03 +00:00
FoxxMD 6f8debc304 docs: Add lastfm endpoint alternative guidance 2026-08-04 18:50:59 +00:00
FoxxMD b53ae78d55 fix(lfm endpoint): Fix no response on no slug matched 2026-08-04 18:30:16 +00:00
FoxxMD 8723bc8890 fix(lfm endpoint): Improve regex slug matching to handle more variants 2026-08-04 18:29:41 +00:00
FoxxMD eca8ad2ce1 fix(lfm endpoint): Fix slug regex match 2026-08-04 18:16:39 +00:00
FoxxMD b6b8d3423f fix(lfm endpoint): Detect desired json response from accept header 2026-08-04 18:06:45 +00:00
FoxxMD fc027e8554 fix(lfm endpoint): remove duplicated method restrictions 2026-08-04 16:07:11 +00:00
FoxxMD 707da834bb docs: regenerate tealfm source env 2026-08-04 15:10:17 +00:00
FoxxMD 002520cd8a docs: Fix config.json example 2026-08-04 15:10:06 +00:00
FoxxMD 7ca84ce27f feat(lfm endpoint): Implement more api emulation #656
* Add optional username/apiKey config fields
* Identify Source by optional username/sk/api_key
* Emulate auth.getMobileSession response with sk derived from Source uid
* Update docs with guidance for multiple sources and improved setup language
2026-08-04 14:59:05 +00:00
FoxxMD 8af56cb561 revert: rollback tealfm source optional app password for now
This needs to be part of a larger refactor to require app password/permissions only for specific operations in the api client
2026-08-03 18:29:03 +00:00
FoxxMD 0e249b13a9 fix(source): Fix player cleanup for ingress sources
This bool check was backward. The scheduled task should no-op for polling sources and run cleanup for ingress sources

#652
2026-08-03 18:25:12 +00:00
FoxxMD 591b5c6618 fix(tealfm): app password for source is optional 2026-08-03 16:55:16 +00:00
FoxxMD 3c5b9cc402 fix(teal): teal client app password is required 2026-08-03 16:52:32 +00:00
FoxxMD 420a053e25 docs: update now playing for clients 2026-08-03 16:06:23 +00:00
FoxxMD 72f64953f6 chore: remove unused ts-zod config 2026-08-03 15:55:00 +00:00
Matt Foxx 8b8c2765c7 Merge pull request #654 from FoxxMD/configBuildSimplify
Simplify config validation and building
2026-08-03 11:53:23 -04:00
FoxxMD e1324921e9 fix: Fix missing name passed to component classes 2026-08-03 15:38:35 +00:00
FoxxMD 9f71fbef97 fix: prevent creating source/client with same id 2026-08-03 15:21:11 +00:00
FoxxMD dd22418c41 fix(tests): Fix missing required id 2026-08-03 14:56:33 +00:00
FoxxMD d296b825d6 Id logging and schema cleanup
* Guard against undefined name when logging component disabled
* Add id/name hint to config location
* inherit name/id field from common schema
2026-08-03 14:31:24 +00:00
FoxxMD 792ccc3c09 feat: Improve location wording for config errors 2026-08-03 13:39:14 +00:00
FoxxMD 6772e9934d refactor!: Enforce mandatory id for file/aio 2026-08-03 13:38:49 +00:00
FoxxMD 4ce7219214 fix: Skip ambigious configs during source parsing if configureAs is not present 2026-08-02 20:21:55 +00:00
FoxxMD b2ce42fecf simplify jellyfin mediatype usage for now 2026-08-01 19:12:04 +00:00
FoxxMD 46a061a602 docs: Update comma-separated list zod types for envs 2026-08-01 19:07:49 +00:00
FoxxMD ff7360fced docs: More env row generation improvements
* Always use `out` of common envs
* Wrapper function for normalizing descriptions with comma-separate lists
2026-08-01 19:07:35 +00:00
FoxxMD 425a0b2807 Partial fix for flattening complex zod types for docs 2026-08-01 17:47:44 +00:00
FoxxMD ad632f38d8 docs: regenerate env docs 2026-08-01 17:31:37 +00:00
FoxxMD 1d1b3b6f56 docs: Update env descriptions for id, name, and enable 2026-08-01 17:31:28 +00:00
FoxxMD 7575e8bff7 fix: Fix unhandled ZodTransform type generation 2026-08-01 17:31:03 +00:00
FoxxMD 31546ed4c1 fix: Early return on component add to prevent dynamic loading 2026-08-01 15:09:52 +00:00
FoxxMD bf9cdc141c re-add clients to aio json schema 2026-08-01 00:05:07 +00:00
FoxxMD 02ab1571b6 refactor: Convert validation client structures to dynamically import 2026-07-31 23:57:59 +00:00
FoxxMD 699bd02782 refactor: Convert validation structures to dynamically import 2026-07-31 23:41:56 +00:00
FoxxMD 0d45d06966 docs: Italicize required env properties 2026-07-31 23:41:12 +00:00
FoxxMD a93d02e1b7 env doc generation fixes 2026-07-31 20:50:41 +00:00
FoxxMD 513e2dde15 feat: Use env config generated from schema 2026-07-31 16:37:59 -04:00
FoxxMD 2fc90dcfda feat: Generate env markdown for components 2026-07-31 20:15:11 +00:00
FoxxMD fd2a178808 feat: Add env prefix to schema interface 2026-07-31 20:08:00 +00:00
FoxxMD 9f54d45576 fix(source): Fix missing defaults 2026-07-31 19:43:00 +00:00
FoxxMD 78aff32334 refactor: Extract source/client building into a function to reduce verbosity 2026-07-31 15:33:34 -04:00
FoxxMD 0115ccb862 refactor: Simplify ScrobbleSources config builder 2026-07-31 19:01:10 +00:00
FoxxMD 8a71663c9f Implement remaining source env schemas 2026-07-31 18:27:01 +00:00
FoxxMD 40eff0b901 refactor: Simplify ScrobbleClients config builder 2026-07-31 17:27:44 +00:00
FoxxMD 13acc56424 refactor client env schemas to reuse zod shape where possible 2026-07-31 10:39:14 -04:00
FoxxMD 09fc0ce15c Implement majority of source env schemas 2026-07-31 10:32:21 -04:00
FoxxMD 4fbdd71588 add EnvSourceSchema interface 2026-07-31 14:03:04 +00:00
FoxxMD 7cf105935e feat: Implement remaining env schemas for clients 2026-07-31 09:59:57 -04:00
FoxxMD 1bd10326eb add generic client env data validation interface 2026-07-31 13:31:52 +00:00
FoxxMD 74605caf15 add interface for env schemas 2026-07-31 13:10:13 +00:00
FoxxMD 7dec9a39e6 chore(release): Bump version for release 2026-07-30 17:50:57 +00:00
FoxxMD 90a8426748 Proof of concept: generate ENV docs from zod schema 2026-07-30 17:19:32 +00:00
FoxxMD 8469d161e9 feat: Reduce db write frequency when idle by buffering date updates #653 2026-07-30 12:37:38 +00:00
Matt Foxx 941f50bff2 Merge pull request #651 from FoxxMD/mbTrackGuard
fix: Guard against tracks/track in mb recording medium
2026-07-30 00:09:59 -04:00
FoxxMD f8d882cfcf docs: Clarify when activity is scrobbled 2026-07-29 19:37:40 +00:00
FoxxMD 0704cebe72 fix(webscrobbler): Fix bad access of PlayerState #650 2026-07-29 15:49:54 +00:00
FoxxMD e43a18bcbb fix: Guard against tracks/track in mb recording medium
Fixes #649
2026-07-29 15:18:45 +00:00
FoxxMD 0baa399e9f lint: Fix new error instantiation 2026-07-29 15:18:02 +00:00
FoxxMD 05b2e1c116 fix: Fix missing logPath base dir argument
NixOS/nixpkgs#519119
2026-07-28 21:22:56 +00:00
FoxxMD a0cdaeb01f docs: Add missing user type to user transform examples 2026-07-28 18:59:49 +00:00
FoxxMD 73c35751ed chore(release): Bump version for RC1 release 2026-07-28 17:27:57 +00:00
FoxxMD 516a8d2178 chore: Cleanup development comments 2026-07-28 17:27:23 +00:00
FoxxMD d595bf9961 chore: Remove zod development package and re-upgrade typescript to 6 2026-07-28 16:49:51 +00:00
FoxxMD 8345779030 refactor: cleaned up aio validation and client/source map 2026-07-28 16:43:51 +00:00
FoxxMD b58d39c90e feat(ui): Implement subsonic playback reporting indicators 2026-07-28 16:14:01 +00:00
FoxxMD 7074ece0a5 docs: Simplify subsonic playback reporting language 2026-07-28 16:13:49 +00:00
FoxxMD bfb0d6b392 refactor: Move platform constants/types to prevent frontend bundling backend code 2026-07-28 16:10:21 +00:00
Matt Foxx 5c3f49fb2f Merge pull request #647 from FoxxMD/zodExperimentRebase
feat: Replace ts-json-schema-generator + ajv with zod
2026-07-27 22:29:32 -04:00
Matt Foxx f70e680d55 Merge branch 'master' into zodExperimentRebase 2026-07-27 22:22:32 -04:00
Matt Foxx c2fda1e7d9 Merge pull request #640 from JannisPohle/feature/ignore-stale-subsonic-play-now-reports
fix: Ignore stale subsonic playing now reports
2026-07-27 22:18:44 -04:00
FoxxMD 70e4239e52 more cache config cleanup 2026-07-28 01:56:26 +00:00
FoxxMD d96073282c feat(cache): Remove deprecated cache configuration
Remove metadata/scrobble/auth config that was deprecated in 0.14.0
2026-07-28 01:56:26 +00:00
FoxxMD 2887600aa0 docs: Clarify language around id/name 2026-07-28 01:56:26 +00:00
FoxxMD 6f7aa6f567 remove unused interface names 2026-07-28 01:56:26 +00:00
FoxxMD 857f44fa14 docs: Add meta descriptions/titles for objects that become refs 2026-07-28 01:56:26 +00:00
FoxxMD d764b0204e fix(docs): Replace old json viewer links 2026-07-28 01:56:26 +00:00
FoxxMD ee2f988357 fix(docs): add aio json with reused refs to reduce file size 2026-07-28 01:56:26 +00:00
FoxxMD de69977d7e fix cf-json-schema-viz package name type 2026-07-28 01:56:26 +00:00
FoxxMD 6eab629168 remove unused patch 2026-07-28 01:56:26 +00:00
FoxxMD f21572bed0 chore: Remove ts-generator-schema rm from dockerfile layer 2026-07-28 01:56:26 +00:00
FoxxMD 98084665ba chore: remove ts-schema-generator and ajv 2026-07-28 01:56:26 +00:00
FoxxMD e8a91bb6de feat: Replace ajv/ts-schema-generator validation with pure zod 2026-07-28 01:56:26 +00:00
FoxxMD 206d1571ac feat(docs): Use zod for all doc schemas and simplify generation/usage 2026-07-28 01:56:26 +00:00
FoxxMD ebf6ec6957 add postCompare description 2026-07-28 01:56:26 +00:00
FoxxMD 03fc3e3a09 feat(docs): Inline defintions to prevent deduplication erasing descriptions 2026-07-28 01:56:26 +00:00
FoxxMD aa50842444 feat(docs): Don't hide top-level descriptions in json viewer 2026-07-28 01:56:26 +00:00
FoxxMD 03dbbdc2cf feat(docs): Patch schema viewer to show more descriptions 2026-07-28 01:56:26 +00:00
FoxxMD bf0b27924e more config annotation 2026-07-28 01:56:26 +00:00
FoxxMD e02a0cc6ae refactor(transform): Simplify transform config by removing untyped stages 2026-07-28 01:56:26 +00:00
FoxxMD ff4dd25a97 refactor(transform)!: Require user stage is typed 2026-07-28 01:56:26 +00:00
FoxxMD 28e0d03e6f Schema generation cleanup and vscode launch config fix 2026-07-28 01:56:22 +00:00
FoxxMD d7f2d6fa8e use zod for client validation 2026-07-28 01:55:25 +00:00
FoxxMD 8f33dc9f28 POC scrobble source validation at runtime 2026-07-28 01:55:25 +00:00
FoxxMD fe068a9216 refactor: Convert MonitorOptions to zod schema after rebase 2026-07-28 01:55:25 +00:00
FoxxMD 06b6e200f5 convert aioconfig 2026-07-28 01:55:25 +00:00
FoxxMD afea13e681 convert barrel client and source configs 2026-07-28 01:55:25 +00:00
FoxxMD f8401faf61 convert webhooks 2026-07-28 01:55:25 +00:00
FoxxMD 7c3590adf9 convert remaining source configs 2026-07-28 01:55:19 +00:00
FoxxMD 742025fecc feat: Convert source and client configs to zod 2026-07-28 01:54:41 +00:00
Jannis Pohle b983de1a58 docs: update subsonic source docs 2026-07-26 06:40:43 +00:00
Jannis Pohle ea77ea435a feat: add support for the playbackReport extension for OpenSubsonic
If the extension is provided by the server and playback data is reported by the client, make use of the new position and state fields when polling the current play entry from subsonic. If the new fields are not provided, fallback to the old timestamp based tracking listen progress.
2026-07-26 06:24:59 +00:00
FoxxMD ecf3273eb9 add zod packages 2026-07-24 16:51:31 -04:00
Jannis Pohle a79b697496 fix: add configuration key for disabling auto-detection of stale songs from subsonic servers via the minutesAgo field 2026-07-24 14:18:06 +00:00
Matt Foxx 79874c6c20 Merge pull request #645 from FoxxMD/ignoreRefactor
refactor: Expand monitoring behavior
2026-07-23 12:30:50 -04:00
FoxxMD aa91d2cb92 fix: Fix missed autoMonitor variable name change 2026-07-23 16:24:17 +00:00
FoxxMD c727445f76 fix: Fix api test fixtures for new api surface with monitoring 2026-07-23 15:32:57 +00:00
FoxxMD c25ca5297c docs: Replace should scrobble with monitoring behavior 2026-07-23 15:08:04 +00:00
FoxxMD 956054f387 refactor(ui): Update legacy ui to be compatible with new monitoring structure 2026-07-23 14:50:34 +00:00
FoxxMD 86ebd3d673 fix(ui): Only render component details when data is present 2026-07-23 14:36:08 +00:00
FoxxMD 0679937b54 feat!: Implement monitoring/ingore activity for all components
* Replace "manual listening behavior" for azuracast/icecast with monitoring functionality for all sources/clients
* Rename "systemScrobble" to "autoMonitor"
* Implement functionality for all sources/clients
2026-07-23 13:41:12 +00:00
Matt Foxx 61a2ff5180 Merge pull request #644 from FoxxMD/isrcNormalization
fix: Normalize ISRC values #643
2026-07-23 08:27:34 -04:00
Matt Foxx 26db80d8c7 Merge pull request #642 from FoxxMD/rockskyAlbumArtistFix
fix(rocksky): Fix missing albumArtist field #632
2026-07-22 15:23:42 -04:00
FoxxMD 212ac27dd3 fix(rocksky): Fix missing albumArtist field #632 2026-07-22 16:01:36 +00:00
Jannis Pohle 084dca73c1 fix: Ignore stale subsonic playing now reports
Ignore playing now entries from subsonic sources when their indicated start time from minutesAgo suggests that playback has been paused or stopped.
2026-07-19 07:01:41 +00:00
228 changed files with 7153 additions and 4282 deletions
+19 -1
View File
@@ -1,6 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Zod Conversion",
"type": "node",
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/ts-zod.ts",
"args": [
"${file}"
],
"runtimeExecutable": "node",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "Run Canonical Replace",
"program": "${workspaceFolder}/docsite/canonical-replace.ts",
@@ -53,7 +71,7 @@
"type": "node",
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/utils/SchemaStaticUtil.ts",
"program": "${workspaceFolder}/src/backend/utils/SchemaDocsStaticUtils.ts",
"runtimeExecutable": "node",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
-2
View File
@@ -115,8 +115,6 @@ 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 \
## peerDependency of validbot and can(?) be removed for production
+4 -2
View File
@@ -22,7 +22,8 @@
"type": "spotify",
"enable": true,
"clients": ["myConfig"],
"name": "mySpotifySource",
"name": "Cool Spotify Name",
"id": "mySpotifyId",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
@@ -34,7 +35,8 @@
{
"type": "maloja",
"enable": true,
"name": "myConfig",
"name": "Foxx Maloja",
"id": "myMalojaId",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
+2 -1
View File
@@ -7,7 +7,8 @@
"url": "http://localhost:4040/airsonic",
"user": "yourUser",
"password": "yourPassword",
"usersAllow": ["yourUser"]
"usersAllow": ["yourUser"],
"detectStaleNowPlayingFromMinutesAgo": true
}
}
]
@@ -0,0 +1,12 @@
| Environmental Variable | Type | Default | Description |
| ---------------------------------- | -------------------------- | --------------------- | --------------------------------------- |
| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` |
| `DISCORD_ENABLE` | boolean | true | Should this component be used? |
| `DISCORD_TOKEN` | string | | |
| `DISCORD_ARTWORK` | boolean OR string OR array | | |
| `DISCORD_APPLICATION_ID` | string | | |
| `DISCORD_IPC_LOCATIONS` | string OR array | | |
| `DISCORD_ARTWORK_DEFAULT_URL` | string | | |
| `DISCORD_STATUS_OVERRIDE_ALLOW` | string OR array | | |
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | string OR array | | |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | --------------------------------------- |
| _**`KOITO_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `KOITO_NAME` | string | Value of `KOITO_ID` | A vanity name EX `My Cool Component` |
| `KOITO_ENABLE` | boolean | true | Should this component be used? |
| _**`KOITO_URL`**_ | string | | URL for the Koito server |
| _**`KOITO_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`KOITO_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------------------------- | ------------------------------------------------------- |
| _**`LASTFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LASTFM_NAME` | string | Value of `LASTFM_ID` | A vanity name EX `My Cool Component` |
| `LASTFM_ENABLE` | boolean | true | Should this component be used? |
| _**`LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account |
| _**`LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account |
| `LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. |
| `LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- |
| _**`LIBREFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LIBREFM_NAME` | string | Value of `LIBREFM_ID` | A vanity name EX `My Cool Component` |
| `LIBREFM_ENABLE` | boolean | true | Should this component be used? |
| `LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account |
| `LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account |
| `LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. |
| `LIBREFM_SESSION` | string | | Optional session id returned from a completed auth flow |
| `LIBREFM_URLBASE` | string | https://libre.fm/2.0/ | (Optional) The host and path prefix for your Libre.fm instance |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- |
| _**`LZ_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LZ_NAME` | string | Value of `LZ_ID` | A vanity name EX `My Cool Component` |
| `LZ_ENABLE` | boolean | true | Should this component be used? |
| `LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default |
| _**`LZ_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`LZ_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | -------------------- | --------------------------------------- |
| _**`MALOJA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name EX `My Cool Component` |
| `MALOJA_ENABLE` | boolean | true | Should this component be used? |
| _**`MALOJA_URL`**_ | string | | URL for maloja server |
| _**`MALOJA_API_KEY`**_ | string | | API Key for Maloja server |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| _**`ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name EX `My Cool Component` |
| `ROCKSKY_ENABLE` | boolean | true | Should this component be used? |
| `ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account |
| `ROCKSKY_TOKEN` | string | | Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account |
| _**`ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ------------------------- | ------- | -------------------- | --------------------------------------------------------------------------------------------- |
| _**`TEALFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name EX `My Cool Component` |
| `TEALFM_ENABLE` | boolean | true | Should this component be used? |
| _**`TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as |
| _**`TEALFM_APP_PW`**_ | string | | The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account |
@@ -17,10 +17,10 @@ A **Client** is an application that stores the historical information about what
| [Last.fm](/configuration/clients/lastfm) | ✅ |
| [Libre.fm](/configuration/clients/librefm) | ✅ |
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
| [Koito](/configuration/clients/koito) | |
| [Koito](/configuration/clients/koito) | |
| [Maloja](/configuration/clients/maloja) | ❌ |
| [Rocksky](/configuration/clients/rocksky) | ❌ |
| [Teal.fm](/configuration/clients/tealfm) | |
| [Teal.fm](/configuration/clients/tealfm) | |
## Features
+2 -7
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_koito.md"
:::tip[Other Uses]
@@ -52,11 +53,5 @@ For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLO
## Configuration
<Config config="KoitoClientConfig" fileContent={JsonConfig} client name="koito">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | --------------------------------- |
| `KOITO_ID` | Yes | | A unique id |
| `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 |
| `KOITO_NAME` | No | | A vanity name, different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_lastfm.md"
:::tip[Other Users]
@@ -63,12 +64,5 @@ The Redirect URL can be explicitly specified in the configuration below, or by u
## Configuration
<Config config="LastfmClientConfig" fileContent={JsonConfig} client name="lastfm">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
| `LASTFM_ID` | Yes | | A unique id |
| `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. |
| `LASTFM_NAME` | No | | A vanity name, different than ID |
<ENVConfig/>
</Config>
+2 -10
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_librefm.md"
:::tip[Other Users]
@@ -54,14 +55,5 @@ https://my.cool.libre.com/2.0/
## Configuration
<Config config="LibrefmClientConfig" fileContent={JsonConfig} client name="librefm">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `LIBREFM_ID` | Yes | | A unique ID |
| `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/` |
| `LIBREFM_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_listenbrainz.md"
:::tip[Other Users]
@@ -23,11 +24,5 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
## Configuration
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
| `LZE_ID` | Yes | | A unique ID |
| `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 |
| `LZ_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_maloja.md"
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
@@ -38,10 +39,5 @@ Finally, add the Maloja server URL and API Key to the configuration type you cho
## Configuration
<Config config="MalojaClientConfig" fileContent={JsonConfig} client name="maloja">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------- |
| `MALOJA_ID` | Yes | | A unique ID |
| `MALOJA_URL` | Yes | | Base URL of your installation |
| `MALOJA_API_KEY` | Yes | | Api Key |
| `MALOJA_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -6,6 +6,7 @@ toc_max_heading_level: 5
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_rocksky.md"
:::tip[Other Uses]
@@ -76,10 +77,5 @@ Keys are now **deprecated** and functionality for them will be removed in a futu
</DetailsAdmo>
<Config config="RockSkyClientConfig" fileContent={JsonConfig} client name="rocksky">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| `ROCKSKY_ID` | Yes | | A unique ID used in MS |
| `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
| `ROCKSKY_TOKEN` | Yes | | Access Token obtained from Rocksky |
| `ROCKSKY_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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";
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_tealfm.md"
:::tip[Other Uses]
@@ -78,10 +79,5 @@ If using a 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 |
| :--------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| `TEAMFM_ID` | Yes | | A unique ID |
| `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 |
| `TEALFM_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+109 -55
View File
@@ -13,7 +13,7 @@ import FileExample from "../../src/components/FileExample";
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
import AIOConfig from '!!raw-loader!../../../config/config.json.example';
import SpotifyConfig from '!!raw-loader!../../../config/spotify.json.example';
import SingleConfig from '!!raw-loader!../../../config/jellyfin.json.example';
:::tip
@@ -29,40 +29,35 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
* client/source specific json config files
* an all-in-one json config file
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one type.
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
MS will parse environmental variables present in the OS/container when it is run. **This method means MS does not require files to run.**
MS will parse environmental variables present in the OS/container when it is run.
This method means MS **does not require config files** but it will still create and use data files in the [local](/installation/#config-and-data-directories) or [docker](/installation/?dockerSetting=storage#recommended-settings) data directory.
<details>
<summary>Use ENV-based configuration if...</summary>
* You are the only person for whom MS is scrobbling for
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Plex -> Maloja
* You are the only person for whom MS is scrobbling for or
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Jellyfin -> Koito, or
* You are not comfortable editing JSON files
</details>
<details>
<summary>Config Example</summary>
For Docker container...
```shell
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...
```
For Docker Compose
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
environment:
- SPOTIFY_CLIENT_ID=yourId
- SPOTIFY_CLIENT_SECRET=yourSecret
- MALOJA_URL=http://domain.tld:42010
- MALOJA_API_KEY=1234
- JELLYFIN_URL=192.168.0.110:8096
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b
- JELLYFIN_USER=MyUser
- JELLYFIN_ID=myJellyin
# ...
# ...
```
@@ -77,77 +72,112 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
<TabItem value="file" label="File">
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
* Local installation -> `PROJECT_DIR/config`
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
* Docker -> `/config` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) for how to configure this correctly
<details>
<summary>Use File-based configuration if...</summary>
* You have many [Sources](/configuration/sources)
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
Funkwhale...
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
* You want only to scrobble to specific **Clients**
* You are comfortable editing JSON files
* You have more than one of the same type of **Source/Client** you want to use EX 2x Last.fm accounts, 3x Jellyfin accounts
* You want activity from specific **Sources** to scrobble to specific **Clients**
* You need to setup more advanced configuration for a Source/Client
* Most Source/Clients only support basic configuration through ENV, all configuration is possible using File/AIO
* Most Source/Clients only support basic configuration through ENV, more configuration is possible using File. AIO enables complete configuration.
</details>
:::tip
* There are **example configurations** for all Source/Client types and AIO config located in the [`/config`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
* For docker installations these examples are copied to your configuration directory on first-time use.
* These are the same examples you will find in the **Configuration** section of each Source/Client
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
:::
Each file is named by the **type** of the Client/Source found in below sections. Each file as an **array** of that type of Client/Source.
Each config file is named after the **type** of the Client/Source. Within each file, a JSON **array** defines all of the instances for that type of Client/Source.
Example directory structure:
```
/CONFIG_DIR
plex.json
spotify.json
maloja.json
jellyfin.json
lastfm.json
koito.json
```
<details>
<summary>Config Example</summary>
<CodeBlock title="CONFIG_DIR/spotify.json" language="json5">{SpotifyConfig}</CodeBlock>
```yaml title="jellyfin.json"
[
{
"name": "Foxx JF Server",
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
}
]
```
</details>
<details>
<summary>Multiple of Same Config Example</summary>
```yaml title="jellyfin.json"
[
{
"name": "Foxx JF Server",
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
},
// highlight-start
{
"name": "Foo's JF Server",
"id": "fooJf",
"data": {
"url": "http://192.168.0.150:8096",
"user": "foo",
"apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"
}
}
// highlight-end
]
```
</details>
</TabItem>
<TabItem value="aio" label="File AIO">
MS will parse an **all-in-one** configuration file located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
MS will parse an **all-in-one** configuration file (`config.json`) located in the directory specified by the `CONFIG_DIR` environmental variable. The file's location defaults to:
* Local installation -> `PROJECT_DIR/config/config.json`
* Docker -> `/config/config.json` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
* Docker -> `/config/config.json` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) for how to configure this correctly
<details>
<summary>Use AIO-based configuration if...</summary>
* You have many [Sources](/configuration/sources)
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
Funkwhale...
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
* You want only to scrobble to specific **Clients**
* You need to setup [monitoring/webhooks](#monitoring)
* You want to setup defaults for all Sources/Clients
* ...All of the reasons listed in [File](./?configType=file#configuration-types)
* You want to setup [monitoring/webhooks](#monitoring), default settings for all Sources/Clients, or global config options
* You want to use [Transformers](/configuration/transforms) (other than what is provided for [Musicbrainz ENV](/configuration/transforms/musicbrainz/#env-configuration))
* You want complete control over all potential MS configuration
</details>
**The AIO config also enables setting default options for sources/clients as well as global options for MS itself.**
:::tip
* An example AIO config files can be found in the project directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example)
* For docker installations this example is copied to your configuration directory on first-time use.
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
* **AIO config is the "complete" way to configure MS.**
* All settings available in File/ENV are available in AIO and AIO has *more* exclusive settings.
* AIO enables configuring **default settings** for all Sources/Clients
* There is a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
* Use the [**Config Playground**](/playground) to explore the entire config schema with an editable example
* The example used can also be found in the repo directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example) in the example below
:::
[**Explore the schema for this configuration, along with an example generator and validator, here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
<details>
<summary>Config Example</summary>
@@ -155,6 +185,30 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
<CodeBlock title="CONFIG_DIR/config.json" language="json5">{AIOConfig}</CodeBlock>
</details>
<DetailsAdmo type="important" summary="Source/Client AIO Configs require a 'type' property">
Compared to their [File](./?configType=file#configuration-types) equivalent, Sources/Clients configured using AIO require an additional `type` property.
```yaml title="jellyfin.json"
[
{
"name": "Foxx JF Server",
// highlight-start
"type: "jellyfin",
// highlight-end
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
}
]
```
You can find an example of the required `type` value by referring to the **AIO** tab in each Source/Client's Configuration section example.
</DetailsAdmo>
</TabItem>
</Tabs>
@@ -248,7 +302,7 @@ WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECR
## Base URL
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
Defines the URL that is used to generate default redirect URLs for authentication for services like [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
* Default => `http://localhost:9078`
* Set with [ENV](./?configType=env#configuration-types) `BASE_URL` or `baseUrl` [all-in-one configuration](./?configType=aio#configuration-types)
@@ -279,7 +333,7 @@ Useful when running with [docker](../installation/installation.mdx#docker) so th
## Caching
Multi-scrobbler implements caching to persist important data across restarts, reduce external API calls, and make some actions faster.
Multi-scrobbler implements caching to reduce external API calls and make some actions faster.
A default **in-memory** cache store is used so that you always benefit from some caching. An optional, [**secondary** store](#secondary-caching) can be configured for greater caching capabilities.
@@ -301,7 +355,7 @@ API Calls to external (metadata) services used to [Enhance Scrobbles](/configura
<DetailsAdmo type="important" summary="Auth Cache Configuration">
Auth caching defaults to a **file** that is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-auth.cache`.
Auth caching defaults to a **file** that is stored in the `DATA_DIR` directory using the pre-defined file name `ms-auth.cache`.
This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)).
@@ -448,7 +502,7 @@ Multi-scrobbler depends on a SQLite database (`ms.db`) that is created on first
The database stores *all* Plays for your Sources/Clients as well as metadata and debugging information to help troubleshoot issues. Each Play is associated with a Source/Config in the database based on your configuration.
You **should set IDs for each Source/Client** so that the database can identify these even when the configuration is changed.
You **must set IDs for each Source/Client** so that the database can identify these even when the configuration is changed.
### Retention
@@ -810,7 +864,7 @@ Webhooks are configured in the AIO [config.json](#configuration-types) file unde
#### [Gotify](https://gotify.net/)
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
Refer to the [config schema for GotifyConfig](/playground)
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
@@ -836,7 +890,7 @@ multi-scrobbler optionally supports setting message notification priority via `i
#### [Ntfy](https://ntfy.sh/)
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
Refer to the [config schema for NtfyConfig](/playground)
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
@@ -864,7 +918,7 @@ multi-scrobbler optionally supports setting message notification priority via `i
#### [Apprise](https://github.com/caronc/apprise-api)
Refer to the [config schema for AppriseConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FAppriseConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
Refer to the [config schema for AppriseConfig](/playground)
multi-scrobbler supports [stateless](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) and [persistent storage](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) endpoints as well as [tags](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging)/
@@ -0,0 +1,13 @@
| Environmental Variable | Type | Default | Description |
| ------------------------------------------ | ------- | ------------------------ | -------------------------------------------------------------------- |
| _**`APPLEMUSIC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `APPLEMUSIC_NAME` | string | Value of `APPLEMUSIC_ID` | A vanity name EX `My Cool Component` |
| `APPLEMUSIC_ENABLE` | boolean | true | Should this component be used? |
| `APPLEMUSIC_KEY_ID` | string | | |
| `APPLEMUSIC_TEAM_ID` | string | | |
| `APPLEMUSIC_KEY_P8` | string | | |
| _**`APPLEMUSIC_MEDIA_USER_TOKEN`**_ | string | | |
| `APPLEMUSIC_TOKEN` | string | | |
| `APPLEMUSIC_ORIGIN_HEADER` | string | | Origin header to include in every Apple Music API request. |
| `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | boolean | true | Fixes a quirk where Apple Music's history API hides duplicate plays. |
| `APPLEMUSIC_NORMALIZE_ALBUM` | boolean | true | Removes extraneous suffixes from album data |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | ------------------------------------------------- |
| _**`AZURA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `AZURA_NAME` | string | Value of `AZURA_ID` | A vanity name EX `My Cool Component` |
| `AZURA_ENABLE` | boolean | true | Should this component be used? |
| _**`AZURA_STATION`**_ | string | | The specific station to monitor |
| _**`AZURA_URL`**_ | string | | Base URL of the Azuracast instance |
| `AZURA_KEY` | string | | API Key used to access data about private streams |
| `AZURA_LISTENERS_NUM` | string | | |
| `AZURA_LIVE` | boolean | | |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ---------------- | ----------------------------------------------------------------------------------------------- |
| _**`CC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `CC_NAME` | string | Value of `CC_ID` | A vanity name EX `My Cool Component` |
| `CC_ENABLE` | boolean | true | Should this component be used? |
| `CC_BLACKLIST_DEVICES` | string | | DO NOT scrobble from any cast devices on this comma-delimited list that START WITH these values |
| `CC_WHITELIST_DEVICES` | string | | ONLY scrobble from any cast device on this comma-delimited list that START WITH these values |
| `CC_BLACKLIST_APPS` | string | | DO NOT scrobble from any application on this comma-delimited list that START WITH these values |
| `CC_WHITELIST_APPS` | string | | ONLY scrobble from any application on this comma-delimited list that START WITH these values |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | -------------------------------------------------------------------- | -------------------------------------------------- |
| _**`DEEZER_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `DEEZER_NAME` | string | Value of `DEEZER_ID` | A vanity name EX `My Cool Component` |
| `DEEZER_ENABLE` | boolean | true | Should this component be used? |
| _**`DEEZER_ARL`**_ | string | | ARL retrieved from Deezer response header |
| `DEEZER_ACCOUNT_ID` | string | | The ID (USER_ID) of the linked account to monitor. |
| `DEEZER_USER_AGENT` | string | Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0 | User agent |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------ |
| _**`LFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LFM_NAME` | string | Value of `LFM_ID` | A vanity name EX `My Cool Component` |
| `LFM_ENABLE` | boolean | true | Should this component be used? |
| `LFM_SLUG` | string | | The URL ending that should be used to identify scrobbles for this source |
| `LFM_USERNAME` | string | | A fake username to differentiate LFM Endpoint Sources |
| `LFM_API_KEY` | string | | A fake api key to differentiate LFM Endpoint Sources |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| _**`LZE_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LZE_NAME` | string | Value of `LZE_ID` | A vanity name EX `My Cool Component` |
| `LZE_ENABLE` | boolean | true | Should this component be used? |
| `LZE_SLUG` | string | | The URL ending that should be used to identify scrobbles for this source |
| `LZE_TOKEN` | string | | If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source |
| `LZE_USERNAME` | string | | The listenbrainz "username" to associate with this Source |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | --------------------- | --------------------------------------- |
| _**`ICECAST_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `ICECAST_NAME` | string | Value of `ICECAST_ID` | A vanity name EX `My Cool Component` |
| `ICECAST_ENABLE` | boolean | true | Should this component be used? |
| _**`ICECAST_URL`**_ | string | | The Icecast stream URL |
| `ICECAST_AUTO_MONITOR` | boolean | | |
@@ -0,0 +1,17 @@
| Environmental Variable | Type | Default | Description |
| -------------------------------- | -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| _**`JELLYFIN_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name EX `My Cool Component` |
| `JELLYFIN_ENABLE` | boolean | true | Should this component be used? |
| _**`JELLYFIN_USER`**_ | string | | The username of the user to authenticate for or track scrobbles for |
| `JELLYFIN_PASSWORD` | string | | Password of the username to authenticate for |
| `JELLYFIN_APIKEY` | string | | API Key to authenticate with. |
| _**`JELLYFIN_URL`**_ | string | | HOST:PORT of the Jellyfin server to connect to |
| `JELLYFIN_USERS_ALLOW` | string OR true | | Only scrobble for specific users from this comma-delimited list. If `true`, scrobble for all users. |
| `JELLYFIN_USERS_BLOCK` | string | | Do not scrobble for users from this comma-delimited list |
| `JELLYFIN_DEVICES_ALLOW` | string | | Only scrobble if device or application name contains strings from this comma-delimited list |
| `JELLYFIN_DEVICES_BLOCK` | string | | Do not scrobble if device or application name contains strings from this comma-delimited list |
| `JELLYFIN_LIBRARIES_ALLOW` | string | | Only scrobble if library name contains string from this comma-delimited list |
| `JELLYFIN_LIBRARIES_BLOCK` | string | | Do not scrobble if library name contains strings from this comma-delimited list |
| `JELLYFIN_FRONTEND_URL_OVERRIDE` | string | | HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from) |
| `JELLYFIN_MEDIATYPES_ALLOW` | string | | Allow media types from this comma-delimited list to be scrobbled |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------------------- | -------------------------------------------------------- |
| _**`JRIVER_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `JRIVER_NAME` | string | Value of `JRIVER_ID` | A vanity name EX `My Cool Component` |
| `JRIVER_ENABLE` | boolean | true | Should this component be used? |
| _**`JRIVER_URL`**_ | string | http://localhost:52199/MCWS/v1/ | URL of the JRiver HTTP server to connect to |
| `JRIVER_USER` | string | | If you have enabled authentication, the username you set |
| `JRIVER_PASSWORD` | string | | If you have enabled authentication, the password you set |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------------------- | ------------------------------------------------- |
| _**`KODI_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `KODI_NAME` | string | Value of `KODI_ID` | A vanity name EX `My Cool Component` |
| `KODI_ENABLE` | boolean | true | Should this component be used? |
| _**`KODI_URL`**_ | string | http://localhost:8080/jsonrpc | URL of the Kodi HTTP server to connect to |
| _**`KODI_USER`**_ | string | | The username set for Remote Control via Web Sever |
| _**`KODI_PASSWORD`**_ | string | | The password set for Remote Control via Web Sever |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| -------------------------- | ------- | -------------------------- | --------------------------------------- |
| _**`SOURCE_KOITO_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_KOITO_NAME` | string | Value of `SOURCE_KOITO_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_KOITO_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_KOITO_URL`**_ | string | | URL for the Koito server |
| _**`SOURCE_KOITO_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`SOURCE_KOITO_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | ------------------------------------- | ------------------------------------------------------- |
| _**`SOURCE_LASTFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_LASTFM_NAME` | string | Value of `SOURCE_LASTFM_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_LASTFM_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account |
| _**`SOURCE_LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account |
| `SOURCE_LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. |
| `SOURCE_LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- |
| _**`SOURCE_LIBREFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_LIBREFM_NAME` | string | Value of `SOURCE_LIBREFM_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_LIBREFM_ENABLE` | boolean | true | Should this component be used? |
| `SOURCE_LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account |
| `SOURCE_LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account |
| `SOURCE_LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. |
| `SOURCE_LIBREFM_SESSION` | string | | Optional session id returned from a completed auth flow |
| `SOURCE_LIBREFM_URLBASE` | string | https://libre.fm/2.0/ | (Optional) The host and path prefix for your Libre.fm instance |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ----------------------- | ------- | ----------------------------- | --------------------------------------------------------- |
| _**`SOURCE_LZ_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_LZ_NAME` | string | Value of `SOURCE_LZ_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_LZ_ENABLE` | boolean | true | Should this component be used? |
| `SOURCE_LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default |
| _**`SOURCE_LZ_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`SOURCE_LZ_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | --------------------------- | --------------------------------------- |
| _**`SOURCE_MALOJA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_MALOJA_NAME` | string | Value of `SOURCE_MALOJA_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_MALOJA_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_MALOJA_URL`**_ | string | | URL for maloja server |
| _**`SOURCE_MALOJA_API_KEY`**_ | string | | API Key for Maloja server |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ----------------------- | ------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
| _**`MOPIDY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MOPIDY_NAME` | string | Value of `MOPIDY_ID` | A vanity name EX `My Cool Component` |
| `MOPIDY_ENABLE` | boolean | true | Should this component be used? |
| `MOPIDY_URL` | string | ws://localhost:6680/mopidy/ws/ | URL of the Mopidy HTTP server to connect to |
| `MOPIDY_URI_DENYLIST` | string | | Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive |
| `MOPIDY_URI_ALLOWLIST` | string | | Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive |
| `MOPIDY_ALBUM_DENYLIST` | string | Soundcloud | Remove album data that matches any case-insensitive string from this list when scrobbling, |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------- |
| _**`MPD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MPD_NAME` | string | Value of `MPD_ID` | A vanity name EX `My Cool Component` |
| `MPD_ENABLE` | boolean | true | Should this component be used? |
| `MPD_URL` | string | localhost:6600 | URL:PORT of the MPD server to connect to |
| `MPD_PASSWORD` | string | | Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------- |
| _**`MPRIS_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name EX `My Cool Component` |
| `MPRIS_ENABLE` | boolean | true | Should this component be used? |
| `MPRIS_BLACKLIST` | string | | DO NOT scrobble from any players that START WITH values from this comma-delimited list |
| `MPRIS_WHITELIST` | string | | ONLY scrobble from any players that START WITH values from this comma-delimited list |
@@ -0,0 +1,6 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | ------------------------------------------------------------ |
| _**`MCAST_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MCAST_NAME` | string | Value of `MCAST_ID` | A vanity name EX `My Cool Component` |
| `MCAST_ENABLE` | boolean | true | Should this component be used? |
| _**`MCAST_URL`**_ | string | | The host or URL of the YamahaExtendedControl endpoint to use |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------- |
| _**`MC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MC_NAME` | string | Value of `MC_ID` | A vanity name EX `My Cool Component` |
| `MC_ENABLE` | boolean | true | Should this component be used? |
| `MC_URL` | string | ws://localhost:7905 | URL of the Musikcube Websocket (Metadata) server to connect to |
| _**`MC_PASSWORD`**_ | string | | Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation |
@@ -0,0 +1,13 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------ | --------------------------------------------------------------------------------------------- |
| _**`PLEX_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name EX `My Cool Component` |
| `PLEX_ENABLE` | boolean | true | Should this component be used? |
| _**`PLEX_URL`**_ | string | | http(s)://HOST:PORT of the Plex server to connect to |
| `PLEX_TOKEN` | string | | |
| `PLEX_USERS_ALLOW` | string | | Only scrobble for specific users from this comma-delimited list |
| `PLEX_USERS_BLOCK` | string | | Do not scrobble for users from this comma-delimited list |
| `PLEX_DEVICES_ALLOW` | string | | Do not scrobble if device or application name contains strings from this comma-delimited list |
| `PLEX_DEVICES_BLOCK` | string | | Only scrobble if device or application name contains strings from this comma-delimited list |
| `PLEX_LIBRARIES_ALLOW` | string | | Only scrobble if library name contains string from this comma-delimited list |
| `PLEX_LIBRARIES_BLOCK` | string | | Do not scrobble if library name contains strings from this comma-delimited list |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| _**`SOURCE_ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_ROCKSKY_NAME` | string | Value of `SOURCE_ROCKSKY_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_ROCKSKY_ENABLE` | boolean | true | Should this component be used? |
| `SOURCE_ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account |
| _**`SOURCE_ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
| _**`SONOS_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name EX `My Cool Component` |
| `SONOS_ENABLE` | boolean | true | Should this component be used? |
| _**`SONOS_HOST`**_ | string | | IP address of any connected Sonos speaker or device |
| `SONOS_DEVICES_ALLOW` | string | | Only scrobble if device name contains strings from this comma-delimited list |
| `SONOS_DEVICES_BLOCK` | string | | Do not scrobble if device name contains strings from this comma-delimited list |
| `SONOS_GROUPS_ALLOW` | string | | Only scrobble if the name of a group the playing device belongs to contains strings from this comma-delimited list |
| `SONOS_GROUPS_BLOCK` | string | | Do not scrobble if the name of a group the playing device belongs to contains strings from this comma-delimited list |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | ------------------------------ | -------------------------------------------------------------------- |
| _**`SPOTIFY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SPOTIFY_NAME` | string | Value of `SPOTIFY_ID` | A vanity name EX `My Cool Component` |
| `SPOTIFY_ENABLE` | boolean | true | Should this component be used? |
| _**`SPOTIFY_CLIENT_ID`**_ | string | | spotify client id |
| _**`SPOTIFY_CLIENT_SECRET`**_ | string | | spotify client secret |
| `SPOTIFY_REDIRECT_URI` | string | http://localhost:9078/callback | spotify redirect URI -- required only if not the default shown here. |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ------------------------- | ------- | ---------------------- | ------------------------------------------------- |
| _**`SUBSONIC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SUBSONIC_NAME` | string | Value of `SUBSONIC_ID` | A vanity name EX `My Cool Component` |
| `SUBSONIC_ENABLE` | boolean | true | Should this component be used? |
| _**`SUBSONIC_USER`**_ | string | | Username to login to the server with |
| _**`SUBSONIC_PASSWORD`**_ | string | | Password for the user to login to the server with |
| _**`SUBSONIC_URL`**_ | string | | URL of the subsonic media server to query |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| -------------------------------- | ------- | --------------------------- | --------------------------------------------------------------------------------------------- |
| _**`SOURCE_TEALFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_TEALFM_NAME` | string | Value of `SOURCE_TEALFM_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_TEALFM_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as |
| _**`SOURCE_TEALFM_APP_PW`**_ | string | | The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | ---------------------------------------- |
| _**`VLC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `VLC_NAME` | string | Value of `VLC_ID` | A vanity name EX `My Cool Component` |
| `VLC_ENABLE` | boolean | true | Should this component be used? |
| `VLC_URL` | string | localhost:8080 | URL:PORT of the VLC server to connect to |
| _**`VLC_PASSWORD`**_ | string | | Password for the server |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ---------------- | ----------------------------------------------------------- |
| _**`WS_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `WS_NAME` | string | Value of `WS_ID` | A vanity name EX `My Cool Component` |
| `WS_ENABLE` | boolean | true | Should this component be used? |
| `WS_BLACKLIST` | string | | Block scrobbling from specific WebScrobbler Connectors |
| `WS_WHITELIST` | string | | Only allow scrobbling from specific WebScrobbler Connectors |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ---------------------- | --------------------------------------------------------------------------- |
| _**`YMBRIDGE_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `YMBRIDGE_NAME` | string | Value of `YMBRIDGE_ID` | A vanity name EX `My Cool Component` |
| `YMBRIDGE_ENABLE` | boolean | true | Should this component be used? |
| _**`YMBRIDGE_URL`**_ | string | | URL of the local Python bridge, for example http://yandex-music-bridge:9980 |
| `YMBRIDGE_API_KEY` | string | | Optional API key sent as X-API-Key to the bridge |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------ |
| _**`YTM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `YTM_NAME` | string | Value of `YTM_ID` | A vanity name EX `My Cool Component` |
| `YTM_ENABLE` | boolean | true | Should this component be used? |
| `YTM_REDIRECT_URI` | string | | Google Cloud Console project OAuth Client Authorized redirect URI |
| `YTM_CLIENT_ID` | string | | Google Cloud Console project OAuth Client ID |
| `YTM_CLIENT_SECRET` | string | | Google Cloud Console project OAuth Client Secret |
| `YTM_COOKIE` | string | | The cookie retrieved from the Request Headers of music.youtube.com after logging in. |
@@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import AppleMusicConfig from '!!raw-loader!@site/../config/applemusic.json.example';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_applemusic.md"
This Source **monitors your Apple Music listening history** via the official Apple Music API and scrobbles new activity to your configured [Clients](/configuration/clients).
@@ -246,17 +247,5 @@ Here is an example of creating a User Stage to only strip suffixes for a specifi
## Configuration Reference
<Config config="AppleMusicSourceConfig" fileContent={AppleMusicConfig} name="applemusic">
| Environment Variable | Required? | Default | Description |
| ------------------------------------------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APPLEMUSIC_ID` | Yes | | A unique ID for this source. |
| `APPLEMUSIC_MEDIA_USER_TOKEN` | Yes | | The media-user-token extracted from the browser. |
| `APPLEMUSIC_KEY_ID` | No | | Key ID from your MusicKit key. |
| `APPLEMUSIC_TEAM_ID` | No | | Team ID from your Apple Developer account. |
| `APPLEMUSIC_KEY_P8` | No | | The contents of your MusicKit `.p8` private key file. |
| `APPLEMUSIC_TOKEN` | No | | The authentication JWT extracted from the browser. |
| `APPLEMUSIC_INTERVAL` | No | `60` | Polling interval in seconds. |
| `APPLEMUSIC_ORIGIN_HEADER` | No | | Origin header for API requests, e.g. `https://music.apple.com`. Required when using a browser token. |
| `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | No | `true` | Apple Music deduplicates its history by bumping re-played tracks to the top. If you listen to Song A → Song B → Song A, the history changes from `[A]` to `[A, B]` (bumping A). MS detects this "top-rebound" to recover the intermediate play (B) and the re-listen (A). Disable this only if you see incorrect duplicate scrobbles. |
| `APPLEMUSIC_NORMALIZE_ALBUM` | No | `true` | Strips extraneous `- EP` and `- Single` suffixes from album names |
| `APPLEMUSIC_NAME` | No | | A vanity name different than the ID. |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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 ENVConfig from "@site/docs/configuration/sources/_env_configs/_azuracast.md"
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
:::tip[Scrobbling Threshold]
@@ -38,14 +39,14 @@ URL From Config => MS Uses
<ShouldScrobble filename="azuracast"/>
Multi-scrobbler will set the default **scrobbling behavior** based on if either of these settings is configured:
When `autoMonitor` is not explicitly set, Multi-scrobbler will set the **default monitoring 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 either is configured then MS will automatically monitor 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.
If neither is configured automatic monitoring defaults to **off** and you should either manually initiate it or explicitly set the default behavior.
### Station
@@ -112,12 +113,5 @@ For users who aren't administrators of Azuracast: if you can access the station
## Configuration
<Config config="AzuracastSourceConfig" fileContent={AzuracastConfig} name="azuracast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
| `AZURA_ID` | Yes | | A unique ID |
| `AZURA_URL` | Yes | | Azuracast URL *without station name* |
| `AZURA_STATION` | Yes | | The station URL Stub or shortcode |
| `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` |
| `AZURA_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_deezer.md"
<Tabs groupId="deezerSource" queryString>
<TabItem value="arl" label="Unofficial ARL">
@@ -134,9 +135,7 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
## Configuration
<Config config="DeezerCompatConfig" fileContent={DeezerConfig} name="deezer">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ------------------------------------------------ |
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
<ENVConfig/>
</Config>
</TabItem>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_chromecast.md"
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.
@@ -149,13 +150,5 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
:::
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------------------------------------------------------ |
| `CC_ID` | Yes | | A unique ID |
| `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 |
| `CC_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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 ENVConfig from "@site/docs/configuration/sources/_env_configs/_icecast.md"
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
Parse metadata from any Icecast Station (streams) to use for scrobbling.
@@ -21,9 +22,9 @@ Multi-scrobbler will do its best to parse Artist/Album/Track information from th
:::
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.
Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, **automatic monitoring for Icecast Sources is off by default.** Use **Monitoring Behavior** to initiate scrobbling or configure automatic scrobble.
<ShouldScrobble filename="icecast"/>
<ShouldScrobble filename="icecast" defaultBehavior={false}/>
:::tip[Scrobbling Threshold]
@@ -64,9 +65,5 @@ If no `sources` are configured for multi-scrobbler it will automatically try to
## Configuration
<Config config="IcecastSourceConfig" fileContent={IcecastConfig} name="icecast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ------------------------------- |
| `ICECAST_ID` | Yes | | A unique ID |
| `ICECAST_URL` | Yes | | Icecast URL |
| `ICECAST_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_jellyfin.md"
Must be using Jellyfin 10.7 or greater
@@ -52,20 +53,5 @@ Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for
#### Configuration
<Config config="JellyApiSourceConfig" fileContent={JellyfinConfig} name="jellyfin">
| Environmental Variable | Required? | Default | Description |
| -------------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `JELLYFIN_ID` | **Yes** | | A unique ID |
| `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` |
| `JELLYFIN_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_jriver.md"
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 be found in the JRiver GUI:
@@ -50,11 +51,5 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi
## Configuration
<Config config="JRiverSourceConfig" fileContent={JriverConfig} name="jriver">
| Environmental Variable | Required | Default | Description |
| ---------------------- | -------- | ------------------------------- | ---------------------------------------------- |
| JRIVER_ID | Yes | | A unique ID |
| 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 |
| JRIVER_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -7
View File
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_kodi.md"
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:
@@ -51,11 +52,5 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi
## Configuration
<Config config="KodiSourceConfig" fileContent={KodiConfig} name="kodi">
| Environmental Variable | Required | Default | Description |
| ---------------------- | -------- | ----------------------------- | ------------------------------- |
| KODI_ID | Yes | | A unique ID |
| 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 |
| KODI_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_koito.md"
This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured [Clients.](/configuration/clients)
@@ -25,11 +26,5 @@ See the [Koito (Client)](/configuration/clients/koito) configuration for general
#### Configuration
<Config config="KoitoSourceConfig" fileContent={KoitoConfig} name="koito">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | --------------------------------- |
| `SOURCE_KOITO_ID` | Yes | | A unique ID |
| `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 |
| `SOURCE_KOITO_NAME` | No | | A vanity name differen than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_endpointlfm.md"
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server.
@@ -18,31 +19,61 @@ This Source enables multi-scrobbler to accept scrobbles from outside application
:::
<DetailsAdmo type="tip" summary="Recommended Alternative">
This Source has the same data limitations as the [Last.fm (Source)](/configuration/sources/lastfm-source) and [Last.fm (Client)](/configuration/clients/lastfm) have: Last.fm does not support separating artists/album artists fields which means any multi-artist tracks will have all artists combined into one string. This makes scrobbling to other services, and using metadata corrections like [Musicbrainz](/configuration/transforms/musicbrainz), more difficult.
If your application has the option to scrobble using Listenbrainz then use the [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint) Source instead.
</DetailsAdmo>
## Setup
### URL
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Last.fm scrobbles at
When setting up your Last.fm client to communicate with Multi-Scrobbler replace the Last.fm domain with your Multi-Scrobbler domain:
https://**last.fm** => https://**yourMSDomain**
MS accepts Last.fm API calls with the same **url base** and structure as the [last.fm api](https://www.last.fm/api/intro), IE `http://yourMSDomain/2.0/`
### Authentication
If you are only setting up **one** Lastfm Endpoint Source then you do not need to configure any explicit username/apiKey/password for MS. If your Last.fm Client requires these credentials use any fake values you want.
<DetailsAdmo type="important" summary="Supported Auth Types">
Currently, Multi-Scrobbler only supports the [**Mobile Application**](https://www.last.fm/api/mobileauth) auth flow. If your client requires one of the other authentication flows please [open an issue](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=02-feature-request.yml).
**Note:** Your client does not **need** to implement any auth in order to use a Lastfm Endpoint Source. You can directly make [`track.scrobble`](https://www.last.fm/api/show/track.scrobble) or [`track.updateNowPlaying`](https://www.last.fm/api/show/track.updateNowPlaying) api calls to `http://yourMSDomain/2.0/` with any fake auth data you want.
</DetailsAdmo>
#### Multiple Sources
If you have **more than one** Lastfm Endpoint Source then you should configure username/apiKey for MS, per Source. This enables MS to differentiate scrobbles for each Source.
Use the same **username** and/or **API Key** you configure with MS when setting up your Last.fm Client. These values can be anything you want, as long as they match between MS and your client. Password can be anything and is not checked.
<DetailsAdmo type="tip" summary="Different URL Base (Slug)">
If you cannot use different username/apiKey per Source (or Last.fm Client) you can still differentiate Sources by using a different **url base (slug)** for Last.fm communication.
Setting a **slug** in config will change the **url base** for MS like this:
```
http://localhost:9078/2.0/
slug: "mySlug"
```
```
http://yourMsDomain/api/lastfm/mySlug
```
which is the "standard" Last.fm server path for scrobbling
The above url base is equivalent to making calls to `http://yourMSDomain/2.0/`
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
```
</DetailsAdmo>
## Configuration
<Config config="LastFMEndpointSourceConfig" fileContent={LastfmEndpointConfig} name="endpointlfm">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `LFM_ID` | Yes | | A unique ID |
| `LFM_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 |
| `LFM_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_lastfm.md"
This Source **monitors your Last.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
@@ -24,12 +25,5 @@ To configure this Source use the same registration instructions and configuratio
## Configuration
<Config config="LastfmSourceConfig" fileContent={LastfmConfig} name="lastfm">
| Environmental Variable | Required? | Default | Description |
| ---------------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
| `SOURCE_LASTFM_ID` | Yes | | A unique ID |
| `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. |
| `SOURCE_LASTFM_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_librefm.md"
This Source **monitors your Libre.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
@@ -23,14 +24,5 @@ To configure this Source use the same registration instructions and configuratio
## Configuration
<Config config="LibrefmSourceConfig" fileContent={JsonConfig} name="librefm">
| Environmental Variable | Required? | Default | Description |
| ----------------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `SOURCE_LIBREFM_ID` | Yes | | A unique ID |
| `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/` |
| `SOURCE_LIBREFM_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_endpointlz.md"
This Source enables multi-scrobbler to accept scrobbles from outside applications **as if it was a Listenbrainz server.**
@@ -109,12 +110,5 @@ To troubleshoot any errors, and assuming you are using Home Assistant, view the
## Configuration
<Config config="ListenbrainzEndpointSourceConfig" fileContent={ListenbrainzEndpointConfig} name="endpointlz">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LZE_ID` | Yes | | A unique ID |
| `LZE_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 |
| `LZE_USERNAME` | No | | (Optional) A fake username that will be returned for token validation and on now-playing responses. If none is provided the Source's name is used instead. |
| `LZE_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_listenbrainz.md"
This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients.
@@ -26,11 +27,5 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
## Configuration
<Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
| `SOURCE_LZ_ID` | Yes | | A unique ID |
| `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 |
| `SOURCE_LZ_NAME` | No | | A vanity name different than ID |d
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_maloja.md"
:::tip[Other Uses]
@@ -22,10 +23,5 @@ See the [Maloja (Client)](/configuration/clients/maloja) configuration for gener
## Configuration
<Config config="MalojaSourceConfig" fileContent={MalojaConfig} name="maloja">
| Environmental Variable | Required? | Default | Description |
| ----------------------- | --------- | ------- | ------------------------------- |
| `SOURCE_MALOJA_ID` | Yes | | A unique ID |
| `SOURCE_MALOJA_URL` | Yes | | Base URL of your installation |
| `SOURCE_MALOJA_API_KEY` | Yes | | Api Key |
| `SOURCE_MALOJA_NAME` | Yes | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mopidy.md"
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/)
@@ -98,5 +99,5 @@ For certain sources (Soundcloud) Mopidy does not have all track info (Album) and
## Configuration
<Config config="MopidySourceConfig" fileContent={MopidyConfig} name="mopidy">
No ENV support
<ENVConfig/>
</Config>
+2 -6
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mpd.md"
MS communicates with MPD using the [TCP client connection.](https://mpd.readthedocs.io/en/stable/user.html#client-connections)
@@ -23,10 +24,5 @@ Multi-scrobbler requires only the [`read`](https://mpd.readthedocs.io/en/stable/
## Configuration
<Config config="MPDSourceConfig" fileContent={MPDConfig} name="mpd">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------- | ------------------------------- |
| `MPD_ID` | Yes | | A unique ID |
| `MPD_URL` | No | `localhost:6600` | |
| `MPD_PASSWORD` | No | | |
| `MPD_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -7
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mpris.md"
[MPRIS](https://specifications.freedesktop.org/mpris-spec/latest) is a standard interface for communicating with Music Players on **linux operating systems.**
@@ -71,11 +72,5 @@ MPRIS communication requires multi-scrobbler to have access to the host's dbus-d
## Configuration
<Config config="MPRISSourceConfig" fileContent={MPRISConfig} name="mpris">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | -------------------------------------------------------------------------------- |
| MPRIS_ID | Yes | | A unique ID |
| 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 |
| MPRIS_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_musikcube.md"
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**:
@@ -31,10 +32,5 @@ 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_ID` | Yes | | A unique ID |
| `MC_URL` | No | `ws://localhost:7905` | Use port set for **metadata server** |
| `MC_PASSWORD` | Yes | | |
| `MC_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -12
View File
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_plex.md"
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.
@@ -194,16 +195,5 @@ To configure MS to use Musicbrainz with your Plex metadata use one of the method
#### Configuration
<Config config="PlexApiSourceConfig" fileContent={PlexConfig} name="plex">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ---------------------------------------------------------------------- |
| `PLEX_ID` | **Yes** | | A unique ID |
| `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 |
| `PLEX_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_rocksky.md"
:::tip[Other Uses]
@@ -33,10 +34,5 @@ See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration f
* [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_ID` | Yes | | A unique ID |
| `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 |
| `SOURCE_ROCKSKY_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -9
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_sonos.md"
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.
@@ -90,13 +91,5 @@ The **optional** `devices` and `groups` related properties found in the config b
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_ID` | Yes | | A unique ID |
| `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 |
| `SONOS_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+51 -35
View File
@@ -13,37 +13,37 @@ A **Source** is a data source that contains information about tracks you are pla
## Sources
| Name | Networking | Scrobble SOT | [Multi Device/User](#multi-deviceuser) | [Scrobble Destination](#limiting-scrobble-destination) | [Thresholds](#scrobble-thresholds) | [Should Scrobble](#should-scrobble-behavior) |
| :---------------------------------------------------------------------- | :------------------------------------------------------- | :-------------------------------------------------- | :------------------------------------- | :----------------------------------------------------- | :--------------------------------- | :------------------------------------------- |
| [Apple Music](/configuration/sources/applemusic) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | |
| [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) | ❌ | ✅ | ❌ | |
| Name | Networking | Scrobble SOT | [Multi Device/User](#multi-deviceuser) | [Scrobble Destination](#limiting-scrobble-destination) | [Thresholds](#scrobble-thresholds) | [Auto Monitoring](#monitoring) |
| :---------------------------------------------------------------------- | :------------------------------------------------------- | :-------------------------------------------------- | :------------------------------------- | :----------------------------------------------------- | :--------------------------------- | :----------------------------- |
| [Apple Music](/configuration/sources/applemusic) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | |
| [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
@@ -123,7 +123,23 @@ Otherwise, use the `name` property you gave the client in it's File/AIO config.
:::
### Scrobble Thresholds
### When Is Activity Scrobbled?
##### From Listening History
For [**Listening History**](./?sot=history#by-data-source-of-truth) Source-Of-Truth based Sources, like Listenbrainz and Last.fm, newely discovered history is immediately forwarded to relevant Clients.
##### From Ingress
For [**Ingress**](./?sot=activity&sourceComm=ingress#by-communication-method) communication based Sources, like [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint) and [Webscrobbler](/configuration/sources/webscrobbler), the client sending data to Multi-Scrobbler is what determines when activity is scrobbled. This means that it up to the behavior and implementation of whatever application you point at these Source endpoints: when the client sends "this is scrobbled", MS immediately forwards it to relevant Clients.
##### From Listening Activity
For Sources that use [**Active**](./?sot=activity&sourceComm=active#by-communication-method) communication with [**Listening Activity**](./?sot=activity#by-data-source-of-truth) as the Source-Of-Truth -- like Jellyfin, Plex, Spotify, Subsonic, etc... -- *Multi-Scrobbler waits until the track changes or the active player becomes stale/disappears* before determining if the activity should be scrobbled.
This allows MS to record how long you listened to the track for as well as more accurately determine if **Scrobble Thresholds** have been met.
#### Scrobble Thresholds
<ScrobbleThreshold filename="spotify"/>
@@ -134,7 +150,7 @@ SOURCE_SCROBBLE_DURATION=40
SOURCE_SCROBBLE_PERCENT=20
```
### Should Scrobble Behavior
### Monitoring
:::note
@@ -142,7 +158,7 @@ This feature is currently available only for some Sources. Check each Source's d
:::
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.
For _most_ Sources multi-scrobbler can determine if a user is listening and monitor activity 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 monitor activity. This action can be taken on the Web Dashboard by clicking the link next to **Monitoring** 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):
@@ -154,7 +170,7 @@ Additionally, for these Sources, the default behavior for Scrobbling can be expl
"options": {
// set to true to always scrobble automatically
// set to false to never scrobble automatically
"systemScrobble": true
"autoMonitor": true
}
}
```
@@ -8,6 +8,7 @@ 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 ENVConfig from "@site/docs/configuration/sources/_env_configs/_spotify.md"
import IconWarning from '@theme/Admonition/Icon/Warning';
<DetailsAdmo type="note" summary="Spotify and Automix">
@@ -111,11 +112,5 @@ Use **one** of the following methods to specify a valid Redirect URI and complet
## Configuration
<Config config="SpotifySourceConfig" fileContent={SpotifyConfig} name="spotify">
| Environmental Variable | Required? | Default | Description |
| ----------------------- | --------- | -------------------------------- | ------------------------------- |
| `SPOTIFY_ID` | Yes | | A unique ID |
| `SPOTIFY_CLIENT_ID` | Yes | | |
| `SPOTIFY_CLIENT_SECRET` | Yes | | |
| `SPOTIFY_REDIRECT_URI` | No | `http://localhost:9078/callback` | URI must end in `callback` |
| `SPOTIFY_NAME` | No | | A vantiy name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_subsonic.md"
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/))
@@ -20,7 +21,7 @@ This Source can be used for any application that implements the [Subsonic API](h
<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.
If your service 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.
@@ -28,14 +29,24 @@ Service-specific scrobble implementations tend to be more accurate and provide m
Use the optional `usersAllow` property with **File** or **AIO** configuration to restrict scrobbling to a list of defined users.
## Enhanced Playback Reporting
Multi-Scrobbler supports the new [Open Subsonic](https://opensubsonic.netlify.app) extension, [Playback Report](https://opensubsonic.netlify.app/docs/extensions/playbackreport/), that enables millisecond-accurate player position and state reporting.
To use this new data, your Subsonic server must support the extension **and** the active playback Client must report it. The UI for your Subsonic Source and active Player will indicate support using contextual icons.
Usage of Enhanced Playback Reporting is optional: MS will fallback to classic, less accurate Subsonic Now Playing when it is not available.
<DetailsAdmo type="info" summary="Stale Players without Playback Reporting">
For Subsonic server/client combinations that do not support [Enhanced Playback Reporting](#enhanced-playback-reporting), multi-scrobbler will cleanup players that report they are playing for much longer period of time than the track's length (when length is reported). This prevents repeated scrobbles from Subsonic-compatible servers that continue returning a track after playback stops.
If the server properly reports no playing song when playback has stopped or this causes issues for your setup, the detection can be disabled by setting `detectStaleNowPlayingFromMinutesAgo` to `false` in File/AIO config.
</DetailsAdmo>
## Configuration
<Config config="SubSonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------ |
| `SUBSONIC_ID` | Yes | | A unique ID |
| `SUBSONIC_USER` | Yes | | |
| `SUBSONIC_PASSWORD` | Yes | | |
| `SUBSONIC_URL` | Yes | | Base url of your subsonic-api server |
| `SUBSONIC_NAME` | No | | A vanity name different than ID |
</Config>
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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 ENVConfig from "@site/docs/configuration/sources/_env_configs/_tealfm.md"
:::tip[Other Uses]
@@ -104,10 +105,5 @@ Allow scrobbles from plyr.fm and unknown services:
</details>
<Config config="TealClientConfig" fileContent={JsonConfig} name="tealfm">
| Environmental Variable | Required? | Default | Description |
| :------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| `SOURCE_TEALFM_ID` | Yes | | A unique ID |
| `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 |
| `SORUCE_TEALFM_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -6
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_vlc.md"
MS communicates with [VLC](https://www.videolan.org/vlc/) using the [Web (`http`) interface module](https://wiki.videolan.org/VLC_HTTP_requests/)
@@ -99,10 +100,5 @@ If you find that VLC is incorrectly reporting track information (in its interfac
## Configuration
<Config config="VLCSourceConfig" fileContent={JsonConfig} name="vlc">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------- | ------------------------------- |
| `VLC_ID` | Yes | | A unique ID |
| `VLC_URL` | No | `localhost:8080` | |
| `VLC_PASSWORD` | Yes | | |
| `VLC_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_webscrobbler.md"
After installing the [Webscrobbler extension](https://web-scrobbler.com/) open the preferences/settings for it:
@@ -87,11 +88,5 @@ This affects **only** MS's behavior and does not affect the general connector be
## Configuration
<Config config="WebScrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------------------------------------------ |
| WS_ID | Yes | | A unique ID |
| 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 |
| WS_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_musiccast.md"
Monitor [MusicCast](https://usa.yamaha.com/products/contents/audio_visual/musiccast/index.html) device/receivers for music played on Network/USB/CD inputs.
@@ -31,9 +32,5 @@ EX
## Configuration
<Config config="MusicCastSourceConfig" fileContent={JsonConfig} name="musiccast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :------------------------------ |
| `MCAST_ID` | Yes | | A unique ID |
| `MCAST_URL` | Yes | | The Musiccast device URL |
| `MCAST_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_ymbridge.md"
Monitor your [**Yandex Music**](https://music.yandex.com) listening activity in real-time.
@@ -72,10 +73,5 @@ If you have issues specifically with the MS Source please mention [**Druidblack*
## Configuration
<Config config="YandexMusicBridgeSourceConfig" fileContent={JsonConfig} name="ymbridge">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :------------------------------------------------------------------------ |
| `YMBRIDGE_ID` | Yes | | A unique ID |
| `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 |
| `YMBRIDGE_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_ytmusic.md"
:::warning
@@ -179,13 +180,5 @@ Only one of these methods needs to be used.
## Configuration
<Config config="YTMusicSourceConfig" fileContent={JsonConfig} name="ytmusic">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | --------------------------------------------- |
| YTM_ID | Yes | | A unique ID |
| 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 |
| YTM_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -67,7 +67,6 @@ In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?co
"preCompare": [
{
"type": "user",
// do not include name, the default user stage will be used
"title": [
// removes badterm from track title
"badterm"
@@ -139,9 +138,9 @@ Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-a
},
"options": {
"playTransform": {
"type": "user",
"preCompare": [
{
"type": "user",
"title": [
// removes "badTerm" from title
"badTerm",
@@ -182,6 +181,7 @@ Using `when` for [Conditional Modification](/configuration/transforms#conditiona
//
// Run live|remastered regex remove on title
{
"type": "user",
"title": [
{
"search": "/\\s\\-\\s滾石40\\s滾石撞樂隊\\s40團拚經典(.+)$/i",
@@ -210,6 +210,7 @@ Removes the phrase `(Album Version)` from the Title of a Play
```json5
{
"type": "user",
"title": [
"(Album Version)"
]
@@ -227,11 +228,13 @@ Removes the phrase `(Album Version)` from the Title of a Play
{
"compare": {
"candidate": {
"type": "user",
"title": [
"/(\(.+\))\s*$/"
]
},
"existing": {
"type": "user",
"title": [
"/(\(.+\))\s*$/"
]
@@ -248,6 +251,7 @@ Removes the phrase `(Album Version)` from the Title of a Play
<summary>Example</summary>
```json5
{
"type": "user",
"artists": [
{
"search": "Boz Skaggs",
@@ -265,6 +269,7 @@ Removes the phrase `(Album Version)` from the Title of a Play
<summary>Example</summary>
```json5
{
"type": "user",
"album": [
{
"search": "Various Artists",
@@ -299,6 +304,7 @@ Removes the phrase `(Album Version)` from the Title of a Play
<summary>Example</summary>
```json
{
"type": "user",
"artists": [
{
"search": "(.*?)(?<delim>\\s*\\/\\s*)(.*$)",
+3 -4
View File
@@ -125,7 +125,7 @@ services:
</TabItem>
<TabItem value="linuxHost" label="Linux Host">
If you are running this container with **Docker** on a **Linux Host** you **should** specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
If you are running this container with **Docker** on a **Linux Host** you **should** specify `user:group` permissions of the user who owns the **configuration and data directories** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
To get the UID and GID for the current user run these commands from a terminal:
@@ -150,7 +150,7 @@ services:
</TabItem>
<TabItem value="caching" label="Caching">
**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration#secondary-caching) to take advantage of faster performance and reduced memory usage.
**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration#secondary-caching) to take advantage of faster performance, reduced memory usage, and cached api calls to external services.
```yaml title="docker-compose.yml"
services:
@@ -215,8 +215,7 @@ services:
- PUID=1000
- PGID=1000
# uncomment along with valkey service/volume below for better caching
#- CACHE_METADATA=valkey
#- CACHE_METADATA_CONN=redis://valkey:6379
#- CACHE_VALKEY=redis://valkey:6379
volumes:
- ./config:/config
ports:
+18 -6
View File
@@ -11,7 +11,7 @@ index 0536c69..be6274b 100644
type JsonSchemaProps = Partial<JSVOptions> & {
diff --git a/node_modules/cf-json-schema-viz/dist/index.js b/node_modules/cf-json-schema-viz/dist/index.js
index b23316c..ef8be1e 100644
index b23316c..d89b3ea 100644
--- a/node_modules/cf-json-schema-viz/dist/index.js
+++ b/node_modules/cf-json-schema-viz/dist/index.js
@@ -41,19 +41,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
@@ -3462,7 +3462,19 @@ index b23316c..ef8be1e 100644
}
return [makeChoice(schemaNode)];
}, [schemaNode]);
@@ -8468,22 +8501,6 @@ var TopLevelSchemaRow = ({
@@ -8313,7 +8346,10 @@ var SchemaRow = React2.memo(
);
const { selectedChoice, setSelectedChoice, choices } = useChoices(schemaNode);
const typeToShow = selectedChoice.type;
- const description = isRegularNode(typeToShow) ? typeToShow.annotations.description : null;
+ let description = isRegularNode(typeToShow) ? typeToShow.annotations.description : null;
+ if ((description === null || description === void 0) && "annotations" in schemaNode && (schemaNode.annotations?.description !== null && schemaNode.annotations?.description !== void 0)) {
+ description = schemaNode.annotations?.description;
+ }
const rootLevel = renderRootTreeLines ? 1 : 2;
const childNodes = React2.useMemo(() => visibleChildren(typeToShow), [typeToShow]);
const combiner = isRegularNode(schemaNode) && schemaNode.combiners?.length ? schemaNode.combiners[0] : null;
@@ -8468,22 +8504,6 @@ var TopLevelSchemaRow = ({
[schemaNode.fragment]
);
const hasVendorProperties = totalVendorExtensions > 0;
@@ -3485,7 +3497,7 @@ index b23316c..ef8be1e 100644
if (isRegularNode(schemaNode) && choices.length > 1) {
const combiner = isRegularNode(schemaNode) && schemaNode.combiners?.length ? schemaNode.combiners[0] : null;
return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -8517,6 +8534,22 @@ var TopLevelSchemaRow = ({
@@ -8517,6 +8537,22 @@ var TopLevelSchemaRow = ({
) : combiner ? /* @__PURE__ */ jsx(SchemaRow, { schemaNode: selectedChoice.type, nestingLevel }) : null
] });
}
@@ -3508,7 +3520,7 @@ index b23316c..ef8be1e 100644
if (isComplexArray(schemaNode) && isPureObjectNode(schemaNode.children[0])) {
const validations = isRegularNode(schemaNode) ? getValidationsFromSchema(schemaNode) : {};
return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -8550,6 +8583,7 @@ var JsonSchemaViewer = ({
@@ -8550,6 +8586,7 @@ var JsonSchemaViewer = ({
disableCrumbs,
nodeHasChanged,
skipTopLevelDescription,
@@ -3516,7 +3528,7 @@ index b23316c..ef8be1e 100644
...rest
}) => {
const options = React2.useMemo(
@@ -8562,7 +8596,8 @@ var JsonSchemaViewer = ({
@@ -8562,7 +8599,8 @@ var JsonSchemaViewer = ({
hideExamples,
renderRootTreeLines,
disableCrumbs,
@@ -3526,7 +3538,7 @@ index b23316c..ef8be1e 100644
}),
[
defaultExpandedDepth,
@@ -8573,7 +8608,8 @@ var JsonSchemaViewer = ({
@@ -8573,7 +8611,8 @@ var JsonSchemaViewer = ({
hideExamples,
renderRootTreeLines,
disableCrumbs,
@@ -11,6 +11,8 @@ import {useWindowSize} from '@docusaurus/theme-common';
<TabItem value="env" label="ENV">
<EnvType>
{props.children}
_**Bold/Italic**_ = Required
</EnvType>
</TabItem>
<TabItem value="file" label="File">
@@ -25,7 +27,7 @@ import {useWindowSize} from '@docusaurus/theme-common';
<DetailsAdmo type="note" summary="Config Structure">
This displays the **structure** of the [File Configuration](/configuration?configType=aio#configuration-types) for a <strong children={props.name}/> type <strong children={props.client ? 'Client' : 'Source'}/> with all possible properties, their shape, and descriptions/types. Use this to understand how to write a valid config.
</DetailsAdmo>
<ExplorerFetch name={`schemas/${props.config}`} />
<ExplorerFetch name={`schemas/${props.name}-${props.client ? 'client' : 'source'}`} />
</div>
<div style={{ flexBasis: 'auto', flexGrow: 3 }}>
<div className="fileEditor">
@@ -51,7 +53,7 @@ import {useWindowSize} from '@docusaurus/theme-common';
filePathName={`CONFIG/${props.name}.json`}
configureAs={props.client ? 'client' : 'source'}
data={props.fileContent}
schemaName={props.config}
schemaName={`${props.name}-${props.client ? 'client' : 'source'}`}
readOnlyAtWidth={500}
/>
</div>
@@ -21,6 +21,7 @@ export default function Explorer(props: ExplorerProps): JSX.Element {
renderRootTreeLines={true}
emptyText="No schema defined"
defaultExpandedDepth={0}
skipTopLevelDescription={false}
markup
/>
</div>
@@ -1,6 +1,6 @@
import CodeBlock from '@theme/CodeBlock';
A **Source** that monitors active listening uses one of two metrics to determine if a Song is scrobbable based on your listening:
A **Source** that monitors Listening Activity uses one of two metrics to determine if a Song is scrobbable based on your listening:
* **percent** of track listened to (50% or more)
* or amount of time (**duration**) track was listened to (4 minutes or more)
@@ -1,5 +1,5 @@
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble.mdx"
<DetailsAdmo type="note" summary="Should Scrobble Behavior">
<DetailsAdmo type="note" summary="Auto Monitoring Behavior">
<ShouldScrobble {...props}/>
</DetailsAdmo>
@@ -1,8 +1,8 @@
import CodeBlock from '@theme/CodeBlock';
This **Source** supports toggling scrobbling on or off manually. This action can be taken on the Web Dashboard by clicking the link next to **Should Scrobble** found in each Source's status card.
This **Source** supports toggling [monitoring on or off manually.](/configuration/sources#monitoring) This action can be taken on the Web Dashboard by clicking the link next to **Monitoring** found in each Source's status card.
Additionally, 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):
Additionally, the **default monitoring behavior** can be explicitly set by adding the following value to a Source's `options` in [file or aio config](/configuration?configType=file#configuration-types):
<CodeBlock
language="json5"
@@ -13,10 +13,17 @@ title={`${props.filename}.json`}>
},
"options": {
// highlight-start
// set to true to always scrobble automatically
// set to false to never scrobble automatically
"systemScrobble": true
// set to true to always monitor (unless manually invoked)
// set to false to never monitor (unless manually invoked)
"autoMonitor": true
// highlight-end
}
}}`}
</CodeBlock>
</CodeBlock>
{props.defaultBehavior === true ? (<span>
This component's default monitoring behavior, when the above is not set, is to <strong>always</strong> monitor.
</span>) : undefined}
{props.defaultBehavior === false ? (<span>
This component's default monitoring behavior, when the above is not set, is to <strong>never</strong> monitor.
</span>) : undefined}
+16 -4
View File
@@ -1,6 +1,15 @@
{
"debugMode": false,
"disableWeb": false,
"sourceDefaults": {
"maxPollRetries": 0,
"logPayload": false,
"logFilterFailure": "warn",
"logPlayerState": false,
"scrobbleThresholds": {
"duration": 30,
"percent": 50
},
"maxPollRetries": 1,
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
@@ -8,12 +17,13 @@
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
"baseUrl": "http://localhost",
"sources": [
{
"type": "spotify",
"enable": true,
"clients": ["myConfig"],
"name": "mySpotifySource",
"name": "Cool Spotify Name",
"id": "mySpotifyId",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
@@ -24,7 +34,9 @@
"clients": [
{
"type": "maloja",
"name": "myConfig",
"enable": true,
"name": "Foxx Maloja",
"id": "myMalojaId",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
+54 -398
View File
@@ -1,12 +1,12 @@
{
"name": "multi-scrobbler",
"version": "0.14.2",
"version": "0.16.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-scrobbler",
"version": "0.14.2",
"version": "0.16.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -31,14 +31,12 @@
"@keyv/valkey": "^1.0.8",
"@lukehagar/plexjs": "^0.39.0",
"@pierre/diffs": "^1.2.4",
"@rocksky/sdk": "^0.2.2",
"@rocksky/sdk": "^0.3.0",
"@supercharge/promise-pool": "^3.0.0",
"@svrooij/sonos": "^2.5.0",
"@xhayper/discord-rpc": "1.3.0",
"abort-controller-x": "^0.5.0",
"address": "^1.2.2",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"avahi-browse": "^1.1.4",
"better-sse": "^0.8.0",
"body-parser": "^2.2.2",
@@ -184,6 +182,7 @@
"git-cliff": "^2.12.0",
"globals": "^17.7.0",
"ky": "^2.0.2",
"markdown-table": "^3.0.4",
"mocha": "^10.3.0",
"mockdate": "^3.0.5",
"msw": "^2.14.6",
@@ -206,12 +205,12 @@
"tailwindcss": "^4.2.2",
"tinyexec": "^1.1.1",
"ts-essentials": "^10.2.0",
"ts-json-schema-generator": "^2.3.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.12",
"with-local-tmp-dir": "^7.0.1"
"with-local-tmp-dir": "^7.0.1",
"zod": "^4.4.3"
},
"engines": {
"node": ">=24.14.0",
@@ -1373,6 +1372,16 @@
}
}
},
"node_modules/@chakra-ui/cli/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@chakra-ui/react": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.36.0.tgz",
@@ -2535,6 +2544,27 @@
"zod-validation-error": "^2.1.0"
}
},
"node_modules/@foxxmd/chromecast-client/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@foxxmd/chromecast-client/node_modules/zod-validation-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-2.1.0.tgz",
"integrity": "sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.18.0"
}
},
"node_modules/@foxxmd/get-version": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@foxxmd/get-version/-/get-version-0.0.3.tgz",
@@ -4461,17 +4491,6 @@
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT"
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -4669,9 +4688,9 @@
}
},
"node_modules/@rocksky/sdk": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@rocksky/sdk/-/sdk-0.2.2.tgz",
"integrity": "sha512-2tZCy7Phd/+gcuSwEfBpnJsLcPYkq+dH9UbZNK0nKxo6tHNWBZfeAJ5zwqUTsasQtQ1lLEYHnO5HInr/koqO2w==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@rocksky/sdk/-/sdk-0.3.0.tgz",
"integrity": "sha512-6gYufTupi21fwJKpNOK5btZ/fGCSufzJqbtigiTIxodsg6xJEwS/bHOr7/XxfXqOy4X798BLOi5Fr4A/CTQfCA==",
"license": "MIT"
},
"node_modules/@rolldown/binding-android-arm64": {
@@ -8594,39 +8613,6 @@
"node": ">= 20"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/anser": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz",
@@ -11018,13 +11004,6 @@
"integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==",
"license": "BSD-3-Clause"
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -12462,22 +12441,6 @@
"fast-string-truncated-width": "^3.0.2"
}
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fast-wrap-ansi": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
@@ -14506,12 +14469,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
@@ -15147,13 +15104,6 @@
"integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -15241,6 +15191,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -17805,15 +17766,6 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/reselect": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
@@ -18843,35 +18795,6 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -18915,20 +18838,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -19339,215 +19248,6 @@
}
}
},
"node_modules/ts-json-schema-generator": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.3.0.tgz",
"integrity": "sha512-t4lBQAwZc0sOJq9LJt3NgbznIcslVnm0JeEMFq8qIRklpMRY8jlYD0YmnRWbqBKANxkby91P1XanSSlSOFpUmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.15",
"commander": "^12.0.0",
"glob": "^10.3.12",
"json5": "^2.2.3",
"normalize-path": "^3.0.0",
"safe-stable-stringify": "^2.4.3",
"tslib": "^2.6.2",
"typescript": "^5.4.5"
},
"bin": {
"ts-json-schema-generator": "bin/ts-json-schema-generator.js"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/ts-json-schema-generator/node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/ts-json-schema-generator/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ts-json-schema-generator/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/ts-json-schema-generator/node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/ts-json-schema-generator/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/ts-json-schema-generator/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/ts-json-schema-generator/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ts-json-schema-generator/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/ts-json-schema-generator/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ts-json-schema-generator/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ts-json-schema-generator/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ts-json-schema-generator/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/ts-json-schema-generator/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ts-mixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
@@ -20563,38 +20263,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -20839,26 +20507,14 @@
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-validation-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-2.1.0.tgz",
"integrity": "sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.18.0"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+8 -9
View File
@@ -1,19 +1,19 @@
{
"name": "multi-scrobbler",
"version": "0.14.2",
"version": "0.16.0",
"type": "module",
"description": "scrobble plays from multiple sources to multiple clients",
"scripts": {
"schema": "npm run -s schema:docs && npm run -s schema:app",
"schema": "npm run -s schema:docs",
"schema:docs": "node src/backend/utils/SchemaDocsStaticUtils.ts",
"schema:app": "node src/backend/utils/SchemaAppStaticUtils.ts",
"circular": "madge --circular --extensions ts src/index.ts",
"test": "npm run -s test:backend",
"test:backend": "mocha --reporter spec",
"dev": "node src/backend/index.ts",
"start": "NODE_ENV=production node src/backend/index.ts",
"build:frontend": "vite build",
"build:backend": "tsc -p src/backend && npm run -s schema:app",
"build:backend:ts": "tsc -p src/backend",
"build:backend": "npm run -s build:backend:ts",
"build": "npm run -s build:backend && npm run -s build:frontend && npm run -s docs:build",
"build:parallel": "concurrently --kill-others-on-fail --names backend,frontend,docs \"npm run -s build:backend\" \"npm run -s build:frontend\" \"npm run docs:build\"",
"docs:install": "cd docsite && npm install --no-audit",
@@ -71,14 +71,12 @@
"@keyv/valkey": "^1.0.8",
"@lukehagar/plexjs": "^0.39.0",
"@pierre/diffs": "^1.2.4",
"@rocksky/sdk": "^0.2.2",
"@rocksky/sdk": "^0.3.0",
"@supercharge/promise-pool": "^3.0.0",
"@svrooij/sonos": "^2.5.0",
"@xhayper/discord-rpc": "1.3.0",
"abort-controller-x": "^0.5.0",
"address": "^1.2.2",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"avahi-browse": "^1.1.4",
"better-sse": "^0.8.0",
"body-parser": "^2.2.2",
@@ -224,6 +222,7 @@
"git-cliff": "^2.12.0",
"globals": "^17.7.0",
"ky": "^2.0.2",
"markdown-table": "^3.0.4",
"mocha": "^10.3.0",
"mockdate": "^3.0.5",
"msw": "^2.14.6",
@@ -246,12 +245,12 @@
"tailwindcss": "^4.2.2",
"tinyexec": "^1.1.1",
"ts-essentials": "^10.2.0",
"ts-json-schema-generator": "^2.3.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.12",
"with-local-tmp-dir": "^7.0.1"
"with-local-tmp-dir": "^7.0.1",
"zod": "^4.4.3"
},
"browserslist": {
"production": [
@@ -115,3 +115,17 @@ index 72fb15d..f6bdfe5 100644
});
return response.json();
}
diff --git a/node_modules/musicbrainz-api/lib/musicbrainz.types.d.ts b/node_modules/musicbrainz-api/lib/musicbrainz.types.d.ts
index 1ebc7b2..e004b9b 100644
--- a/node_modules/musicbrainz-api/lib/musicbrainz.types.d.ts
+++ b/node_modules/musicbrainz-api/lib/musicbrainz.types.d.ts
@@ -156,7 +156,8 @@ export interface IMedium {
title: string;
format?: string;
'format-id': string;
- tracks: ITrack[];
+ tracks?: ITrack[];
+ track?: ITrack[]
'track-count': number;
'track-offset': number;
position: number;
@@ -1,39 +0,0 @@
diff --git a/node_modules/ts-json-schema-generator/dist/src/AnnotationsReader/ExtendedAnnotationsReader.js b/node_modules/ts-json-schema-generator/dist/src/AnnotationsReader/ExtendedAnnotationsReader.js
index 107dc62..d8cda3c 100644
--- a/node_modules/ts-json-schema-generator/dist/src/AnnotationsReader/ExtendedAnnotationsReader.js
+++ b/node_modules/ts-json-schema-generator/dist/src/AnnotationsReader/ExtendedAnnotationsReader.js
@@ -49,7 +49,7 @@ class ExtendedAnnotationsReader extends BasicAnnotationsReader_js_1.BasicAnnotat
.replace(/\r/g, "")
.trim();
const description = markdownDescription.replace(/(?<=[^\n])\n(?=[^\n*-])/g, " ").trim();
- return this.markdownDescription ? { description, markdownDescription } : { description };
+ return this.markdownDescription ? { description: markdownDescription } : { description };
}
getTypeAnnotation(node) {
const symbol = (0, symbolAtNode_js_1.symbolAtNode)(node);
diff --git a/node_modules/ts-json-schema-generator/dist/src/SchemaGenerator.js b/node_modules/ts-json-schema-generator/dist/src/SchemaGenerator.js
index e3b7761..93f914a 100644
--- a/node_modules/ts-json-schema-generator/dist/src/SchemaGenerator.js
+++ b/node_modules/ts-json-schema-generator/dist/src/SchemaGenerator.js
@@ -95,7 +95,7 @@ class SchemaGenerator {
children.reduce((definitions, child) => {
const name = child.getName();
if (!(name in definitions)) {
- definitions[name] = this.typeFormatter.getDefinition(child.getType());
+ definitions[name] = {...this.typeFormatter.getDefinition(child.getType()), title: name};
}
return definitions;
}, childDefinitions);
diff --git a/node_modules/ts-json-schema-generator/src/AnnotationsReader/ExtendedAnnotationsReader.ts b/node_modules/ts-json-schema-generator/src/AnnotationsReader/ExtendedAnnotationsReader.ts
index ba8fd57..806396c 100644
--- a/node_modules/ts-json-schema-generator/src/AnnotationsReader/ExtendedAnnotationsReader.ts
+++ b/node_modules/ts-json-schema-generator/src/AnnotationsReader/ExtendedAnnotationsReader.ts
@@ -57,7 +57,7 @@ export class ExtendedAnnotationsReader extends BasicAnnotationsReader {
const description = markdownDescription.replace(/(?<=[^\n])\n(?=[^\n*-])/g, " ").trim();
- return this.markdownDescription ? { description, markdownDescription } : { description };
+ return this.markdownDescription ? { description: markdownDescription } : { description };
}
private getTypeAnnotation(node: ts.Node): Annotations | undefined {
const symbol = symbolAtNode(node);
+56 -7
View File
@@ -3,7 +3,7 @@ import {
cacheFunctions,
} from "@foxxmd/regex-buddy-core";
import type EventEmitter from "events";
import type {ComponentType, LifecycleInput, LifecycleStep, PlayData, PlayObject} from "../../core/Atomic.ts";
import {MONITORING_ORIGIN_SYSTEM, MONITORING_ORIGIN_USER, type ComponentType, type LifecycleInput, type LifecycleStep, type PlayData, type PlayObject} from "../../core/Atomic.ts";
import { buildTrackString } from "../../core/StringUtils.ts";
import type {CommonClientConfig} from "./infrastructure/config/client/index.ts";
import type {CommonSourceConfig} from "./infrastructure/config/source/index.ts";
@@ -33,10 +33,10 @@ import { getRetentionCompactAfterFromEnv, getRetentionDeleteAfterFromEnv, isComp
import type {DbConcrete} from "./database/drizzle/drizzleUtils.ts";
import type {ComponentSelect} from "./database/drizzle/drizzleTypes.ts";
import { DrizzlePlayRepository } from "./database/drizzle/repositories/PlayRepository.ts";
import type {ClientType} from "../../core/Atomic.ts";
import type {ClientType, MonitoringStatus} from "../../core/Atomic.ts";
import type {SourceType} from "../../core/Atomic.ts";
import { DrizzleComponentRepository } from "./database/drizzle/repositories/ComponentRepository.ts";
import dayjs from "dayjs";
import dayjs, { type Dayjs } from "dayjs";
import { COMPONENT_STATE, type ComponentCommonApi, type ComponentCommonApiJson, type ComponentState, type PlayApiCommonDetailed } from "../../core/Api.ts";
import type {WebhookPayload} from "./infrastructure/config/health/webhooks.ts";
import type { MarkRequired } from "ts-essentials";
@@ -60,10 +60,17 @@ export default abstract class AbstractComponent extends AbstractInitializable {
status: string = 'Waiting to initialize...';
emitter: EventEmitter;
monitoringActivity?: boolean | undefined;
monitoringActivityDefault: boolean = true;
protected componentType: ComponentType;
type: ClientType | SourceType;
name: string;
lastActiveAt?: Dayjs
lastReadyAt?: Dayjs;
protected lastUpdatedComponentDatesAt?: Dayjs
protected constructor(config: AbstractComponentConfig) {
super(config);
this.transformManager = config.transformManager ?? getRoot().items.transformerManager;
@@ -109,6 +116,8 @@ export default abstract class AbstractComponent extends AbstractInitializable {
name: this.config?.name ?? this.name
});
this.componentId = this.dbComponent.id;
this.lastActiveAt = this.dbComponent.lastActiveAt;
this.lastReadyAt = this.dbComponent.lastReadyAt;
return true;
}
@@ -234,8 +243,7 @@ export default abstract class AbstractComponent extends AbstractInitializable {
if(data === undefined) {
return undefined;
}
// default to user transform type for backward compatibility
const partArr = (Array.isArray(data) ? data : [data]).map(x => ({type: 'user', ...x}));
const partArr = (Array.isArray(data) ? data : [data]);
return partArr.map(x => this.transformManager.parseTransformerConfig(x));
}
@@ -540,10 +548,11 @@ export default abstract class AbstractComponent extends AbstractInitializable {
name: this.dbComponent.name,
state,
mode: this.dbComponent.mode,
monitoringStatus: this.getMonitoringStatus(),
countNonLive: this.dbComponent.countNonLive,
createdAt: this.dbComponent.createdAt?.toISOString(),
lastReadyAt: this.dbComponent.lastReadyAt?.toISOString(),
lastActiveAt: this.dbComponent.lastActiveAt?.toISOString(),
lastReadyAt: this.lastReadyAt?.toISOString(),
lastActiveAt: this.lastActiveAt?.toISOString(),
error: this.error !== undefined && this.error instanceof Error ? serializeError(this.error) : this.error,
warning: this.warning !== undefined && this.warning instanceof Error ? serializeError(this.warning) : this.warning,
...this.additionalApiData()
@@ -585,4 +594,44 @@ export default abstract class AbstractComponent extends AbstractInitializable {
this.status = status;
this.emitComponentUpdate({status});
}
public getSystemMonitoring = (): boolean => this.config.options?.autoMonitor ?? this.getSystemDefaultMonitoring();
protected getSystemDefaultMonitoring = (): boolean => this.monitoringActivityDefault;
public isMonitoring = (): boolean => this.monitoringActivity ?? this.getSystemMonitoring();
public getMonitoringStatus = (): MonitoringStatus => ({
monitoring: this.isMonitoring(),
origin: this.monitoringActivity !== undefined ? MONITORING_ORIGIN_USER : MONITORING_ORIGIN_SYSTEM
});
protected async updateDates(data: {lastActiveAt?: Dayjs, lastReadyAt?: Dayjs, force?: boolean}) {
const {
lastActiveAt,
lastReadyAt,
force = false
} = data;
let updated = false;
if(lastActiveAt !== undefined && lastActiveAt !== this.lastActiveAt) {
this.lastActiveAt = lastActiveAt;
updated = true;
}
if(lastReadyAt !== undefined && lastReadyAt !== this.lastReadyAt) {
this.lastReadyAt = lastReadyAt;
updated = true;
}
if(updated && (
force
|| this.lastUpdatedComponentDatesAt === undefined
|| Math.abs(this.lastUpdatedComponentDatesAt.diff(dayjs(), 's')) >= 60
)
) {
await this.componentRepo.updateById(this.dbComponent.id, {
lastActiveAt: this.lastActiveAt,
lastReadyAt: this.lastReadyAt
});
this.lastUpdatedComponentDatesAt = dayjs();
}
}
}
+6 -57
View File
@@ -14,7 +14,7 @@ import { getConfigDir } from './index.ts';
import path from 'path';
import { cacheFunctions } from "@foxxmd/regex-buddy-core";
import { fileOrDirectoryIsWriteable } from '../utils/FSUtils.ts';
import { asCacheConfig, type CacheAuthProvider, type CacheConfig, type CacheConfigOptions, type CacheConfigUser, type CacheScrobbleProvider } from './infrastructure/Atomic.ts';
import { asCacheConfig, type CacheAuthProvider, type CacheConfig, type CacheConfigOptions, type CacheConfigUser } from './infrastructure/Atomic.ts';
import { Typeson } from 'typeson';
import { builtin } from 'typeson-registry';
import { loggerNoop } from './MaybeLogger.ts';
@@ -40,14 +40,6 @@ typeson.register({
]
});
const unsupportedEnvKeys = [
'CACHE_AUTH_CONN',
'CACHE_METADATA',
'CACHE_SCROBBLE',
'CACHE_SCROBBLE_CONN',
'CACHE_AUTH_CONN'
];
export class MSCache {
config: Required<CacheConfigOptions>
@@ -89,7 +81,6 @@ export class MSCache {
connection: aConn,
//...restAuth
} = {},
regex = 200,
} = config;
this.config = {
@@ -104,11 +95,10 @@ export class MSCache {
auth: {
provider: aProvider,
connection: aConn,
},
regex
}
};
this.regexCache = cacheFunctions(this.config.regex);
this.regexCache = cacheFunctions(200);
// for testing we default to in memory
const inMemory = new Cacheable({primary: initMemoryCache({lruSize: 500, ttl: '1m'})});
@@ -515,48 +505,13 @@ const noopKeyv: KeyvStoreAdapter = {
export const parseUserConfig = (config: CacheConfigUser = {}, parentLogger: Logger = loggerNoop): CacheConfigOptions => {
const logger = childLogger(parentLogger, 'Cache');
let valkeyEnvVal: string | undefined = nonEmptyStringOrDefault(process.env.CACHE_VALKEY);
if(valkeyEnvVal === undefined) {
valkeyEnvVal = nonEmptyStringOrDefault(process.env.CACHE_METADATA_CONN);
if(valkeyEnvVal !== undefined) {
logger.warn('ENV CACHE_METADATA_CONN is deprecated! Replace it with CACHE_VALKEY');
}
}
for(const key of unsupportedEnvKeys) {
if(nonEmptyStringOrDefault(process.env[key]) !== undefined) {
logger.warn(`ENV ${key} is no longer supported. Refer to the Caching docs.`);
}
}
const valkeyEnvVal: string | undefined = nonEmptyStringOrDefault(process.env.CACHE_VALKEY);
const {
valkey = valkeyEnvVal,
// metadata: {
// provider: mProvider = (process.env.CACHE_METADATA as (CacheMetadataProvider | undefined) ?? false),
// connection: mConn = process.env.CACHE_METADATA_CONN,
// //...restMetadata
// } = {},
scrobble: {
provider: sProvider = (process.env.CACHE_SCROBBLE as (CacheScrobbleProvider | undefined) ?? 'file'),
connection = (process.env.CACHE_SCROBBLE_CONN ?? getConfigDir()),
...restScrobble
} = {},
auth: {
provider: aProvider = (process.env.CACHE_AUTH as (CacheAuthProvider | undefined) ?? 'file'),
//...restAuth
} = {},
regex = 200,
} = {}
} = config;
if(config.metadata !== undefined) {
logger.warn('Configuring cache.metadata is no longer supported. Refer to the Caching docs.');
}
if(config.scrobble !== undefined) {
logger.warn('Configuring cache.scrobble is no longer supported. Refer to the Caching docs.');
}
if(config.auth?.connection !== undefined) {
logger.warn('Configuring cache.auth.connection is no longer supported. Refer to the Caching docs.');
}
let authConn: string,
authProvider = aProvider;
@@ -581,15 +536,9 @@ export const parseUserConfig = (config: CacheConfigUser = {}, parentLogger: Logg
provider: valkey !== undefined ? 'valkey' : false,
connection: valkey,
},
scrobble: {
provider: sProvider,
connection,
...restScrobble
},
auth: {
provider: authProvider,
connection: authConn,
},
regex
}
};
}
+9 -10
View File
@@ -2,15 +2,14 @@ import { getDataDir } from '../index.ts';
import * as path from 'path';
import { childLogger, type Logger } from '@foxxmd/logging';
import { loggerNoop } from '../MaybeLogger.ts';
import { COMPACTABLE, type CompactableProperty, DEFAULT_RETENTION_COMPACT_AFTER, DEFAULT_RETENTION_DELETE_AFTER, type RetentionConfigValue, type RetentionOption, type RetentionValue, type RetentionValueUnparsed } from '../infrastructure/config/database.ts';
import type {DurationValue} from '../infrastructure/Atomic.ts';
import type {Duration} from 'dayjs/plugin/duration.js';
import { COMPACTABLE, type CompactableProperty, DEFAULT_RETENTION_COMPACT_AFTER, DEFAULT_RETENTION_DELETE_AFTER, type RetentionConfigValueDurationValue, type RetentionConfigValueDuration, type RetentionOptionDuration, type RetentionOptionRetentionValue, type RetentionValue, type RetentionValueUnparsed } from '../infrastructure/config/database.ts';
import dayjs from 'dayjs';
import { parseDurationFromDurationValue } from '../../utils/TimeUtils.ts';
import assert from 'node:assert';
import * as sqlite from 'node:sqlite';
import { parseBoolStrict } from '../../utils.ts';
import { SimpleError } from '../errors/MSErrors.ts';
import type { Duration } from 'dayjs/plugin/duration.js';
export const MEMORY_DB_NAME = ':memory:';
export const isMemoryDb = (name: string): boolean => name === MEMORY_DB_NAME;
@@ -52,7 +51,7 @@ export const backupDb = async (db: sqlite.DatabaseSync, dbPath: string, opts: {
logger.info('Backed up!');
}
const parseRetentionValue = (val: RetentionValueUnparsed): RetentionValue => {
const parseRetentionValue = (val: RetentionValueUnparsed | Duration): RetentionValue => {
if(typeof val === 'string' || typeof val === 'boolean') {
try {
const boolVal = parseBoolStrict(val);
@@ -69,7 +68,7 @@ const parseRetentionValue = (val: RetentionValueUnparsed): RetentionValue => {
throw new SimpleError('retention value be of one: false, number, or string');
}
const parseRetentionFromEnv = (type: string, defaultVal: number = DEFAULT_RETENTION_DELETE_AFTER): RetentionOption<RetentionValue> => {
const parseRetentionFromEnv = (type: string, defaultVal: number = DEFAULT_RETENTION_DELETE_AFTER): RetentionOptionRetentionValue => {
const deleteAfterEnv = process.env[`RETENTION_${type}_AFTER`] ?? defaultVal,
deleteCompletedEnv = process.env[`RETENTION_${type}_COMPLETED_AFTER`] ?? deleteAfterEnv,
deleteFailedEnv = process.env[`RETENTION_${type}_FAILED_AFTER`] ?? deleteAfterEnv,
@@ -82,14 +81,14 @@ const parseRetentionFromEnv = (type: string, defaultVal: number = DEFAULT_RETENT
}
}
const isRetentionOptionDurations = (val: RetentionOption<RetentionValue>): val is RetentionOption<Duration> => {
const isRetentionOptionDurations = (val: RetentionOptionRetentionValue): val is RetentionOptionDuration => {
return dayjs.isDuration(val.completed)
&& dayjs.isDuration(val.duped)
&& dayjs.isDuration(val.failed);
}
let retentionDeleteAfterFromEnv: RetentionOption<Duration>,
retentionCompactAfterFromEnv: RetentionOption<RetentionValue>;
let retentionDeleteAfterFromEnv: RetentionOptionDuration,
retentionCompactAfterFromEnv: RetentionOptionRetentionValue;
export const getRetentionDeleteAfterFromEnv = () => {
if (retentionDeleteAfterFromEnv === undefined) {
@@ -110,7 +109,7 @@ export const getRetentionCompactAfterFromEnv = () => {
return retentionCompactAfterFromEnv;
}
export const parseRetentionOptions = (opts: RetentionConfigValue<DurationValue> = {}, defaults: RetentionOption<RetentionValue>): RetentionOption<RetentionValue> => {
export const parseRetentionOptions = (opts: RetentionConfigValueDurationValue = {}, defaults: RetentionOptionRetentionValue): RetentionOptionRetentionValue => {
if (typeof opts === 'number' || typeof opts === 'string') {
const dur = parseDurationFromDurationValue(opts);
return {
@@ -145,7 +144,7 @@ export const parseRetentionOptions = (opts: RetentionConfigValue<DurationValue>
}
}
export const parseRetentionOptionsDurations = (opts: RetentionConfigValue<Exclude<RetentionValueUnparsed, false>> = {}, defaults: RetentionOption<Duration>): RetentionOption<Duration> => {
export const parseRetentionOptionsDurations = (opts: RetentionConfigValueDurationValue | RetentionConfigValueDuration = {}, defaults: RetentionOptionDuration): RetentionOptionDuration => {
if (typeof opts === 'number' || typeof opts === 'string') {
const dur = parseDurationFromDurationValue(opts);
return {
+36 -26
View File
@@ -11,6 +11,7 @@ import type { ReportedPlayerStatus, SourceType } from "../../../core/Atomic.ts";
import type { ClientType } from "../../../core/Atomic.ts";
import assert from 'assert';
import * as path from 'path';
import * as z from 'zod';
export const __filename = import.meta.filename;
export const projectRootDir = path.resolve(__filename, '../../../../../');
@@ -108,12 +109,6 @@ export type GroupedPlays = TupleMap<DeviceId,PlayUserId,ProgressAwarePlayObject[
export type GroupedFixedPlays = TupleMap<DeviceId,PlayUserId,FixedSizeList<ProgressAwarePlayObject>>;
export const NO_DEVICE = 'NoDevice';
export const NO_USER = 'SingleUser';
export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER];
export const SINGLE_USER_PLATFORM_ID_STR = `${NO_DEVICE}-${NO_USER}`;
export interface ScrobbledPlayObject {
play: PlayObject
scrobble: PlayObjectMinimal
@@ -270,27 +265,18 @@ export interface CacheConfigOptions {
metadata?: CacheMetadataConfig;
scrobble?: CacheScrobbleConfig;
auth?: CacheAuthConfig;
/** Number of regex functions to cache (LRU)
*
* @default 200
*/
regex?: number
}
export interface CacheConfigUser {
auth?: {
provider: 'valkey' | 'file',
[key: string]: any
};
valkey?: string
/** Number of regex functions to cache (LRU)
*
* @default 200
*/
regex?: number
// to allow deprecated scrobble config without having it show up in schema docs
[key: string]: any
}
export const cacheConfigUserAuthSchema = z.object({
provider: z.enum(['valkey', 'file']),
}).meta({description: 'The cache type to use for Auth'});
export const cacheConfigUserSchema = z.object({
auth: cacheConfigUserAuthSchema.optional(),
valkey: z.string().optional().meta({description: 'The connection string to a valkey server with the syntax `redis://HOST_IP:HOST_PORT`'})
}).meta({title: 'Cache Config', description: 'Configuration for Caching'});
export type CacheConfigUser = z.infer<typeof cacheConfigUserSchema>;
export interface MusicbrainzApiConfigData {
url?: string
@@ -409,6 +395,29 @@ export interface ScrobbleRangeResult {
export const REFRESH_STALE_DEFAULT = 60;
/** A number of seconds */
export const secondsSchema = z.number().meta({
description: 'number of seconds',
examples: [60, 3600],
title: 'Seconds'
});
/** A string representing a number and unit of time comptabile with dayjs */
export const durationStringSchema = z.string().meta({
description: 'a string containing a number and a unit of time compatible with dayjs',
examples:["1 hour", "4 days"],
title: 'Duration Value'
})
export const durationValueSchema = z.xor([secondsSchema, durationStringSchema]).meta({title: 'Duration Value'});
// .meta({
// description: stripIndents`A duration of time as either
// * a number of seconds
// * or a string containing a number and a unit of time compatible with dayjs`,
// example: [60, 3600, "1 hour", "4 days"]});
/**
* A duration of time
*
@@ -419,7 +428,8 @@ export const REFRESH_STALE_DEFAULT = 60;
*
* @example [60, 3600, "1 hour", "4 days"]
*/
export type DurationValue = number | string;
export type DurationValue = z.infer<typeof durationValueSchema>; // number | string;
export type MigrationStatus = {
backupRequired: boolean,
@@ -1,28 +1,49 @@
import type { LogOptions } from "@foxxmd/logging";
import type { ClientAIOConfig } from "./client/clients.ts";
import type { CommonClientOptions } from "./client/index.ts";
import type { RequestRetryOptions } from "./common.ts";
import type { WebhookConfig } from "./health/webhooks.ts";
import type { CommonSourceOptions, SourceRetryOptions } from "./source/index.ts";
import type { SourceAIOConfig } from "./source/sources.ts";
import type { CacheConfigUser, DurationValue } from "../Atomic.ts";
import type { TransformerCommonConfig } from "../../../../core/Atomic.ts";
import type { RetentionConfig } from "./database.ts";
import * as z from "zod";
import {commonClientOptionsSchema} from "./client/index.ts";
import {requestRetryOptionsSchema} from "./common.ts";
import {webhookConfigSchema} from "./health/webhooks.ts";
import {commonSourceOptionsSchema, fileLogOptionsSchema, logLevelSchema, sourceRetryOptionsSchema} from "./source/index.ts";
import {cacheConfigUserSchema} from "../Atomic.ts";
import {retentionConfigDurationValueSchema} from "./database.ts";
export const sourceDefaultsSchema = z.object({
...commonSourceOptionsSchema.shape,
});
export interface SourceDefaults extends CommonSourceOptions {
}
export type SourceDefaults = z.infer<typeof sourceDefaultsSchema>;
export interface ClientDefaults extends CommonClientOptions {
}
export const clientDefaultsSchema = z.object({
...commonClientOptionsSchema.shape,
});
export interface AIOConfig {
sourceDefaults?: SourceDefaults
clientDefaults?: ClientDefaults
sources?: SourceAIOConfig[]
clients?: ClientAIOConfig[]
export type ClientDefaults = z.infer<typeof clientDefaultsSchema>;
webhooks?: WebhookConfig[]
const transformOptionsSchema = z.object({
failOnFetch: z.boolean().optional(),
throwOnFailure: z.union([
z.boolean(),
z.array(z.union([z.literal('artists'), z.literal('title'), z.literal('albumArtists'), z.literal('album'), z.literal('duration'), z.literal('meta'), z.literal('art')])),
]).optional(),
ttl: z.string().optional(),
});
const looseComponent = z.looseObject({ type: z.string(), configureAs: z.string().optional() });
const transformerCommonConfigSchema = z.object({
defaults: z.record(z.string(), z.any()).optional(),
data: z.record(z.string(), z.any()).optional(),
type: z.string(),
name: z.string().optional(),
options: transformOptionsSchema.optional(),
});
export const aioConfigSchema = z.object({
sourceDefaults: sourceDefaultsSchema.optional(),
clientDefaults: clientDefaultsSchema.optional(),
sources: z.array(looseComponent).optional(),
clients: z.array(looseComponent).optional(),
webhooks: z.array(webhookConfigSchema).optional(),
/**
* Set the port the multi-scrobbler UI will be served from
@@ -30,7 +51,11 @@ export interface AIOConfig {
* @default 9078
* @examples [9078]
* */
port?: number
port: z.number().optional().meta({
description: "Set the port the multi-scrobbler UI will be served from",
default: 9078,
examples: [9078]
}),
/**
* Set the Base URL the application should assume the UI is served from.
@@ -44,16 +69,26 @@ export interface AIOConfig {
* @default "http://localhost"
* @examples ["http://localhost", "http://192.168.0.101", "https://ms.myDomain.tld"]
* */
baseUrl?: string
baseUrl: z.string().optional().meta({
description: "Set the Base URL the application should assume the UI is served from.",
default: "http://localhost",
examples: ["http://localhost", "http://192.168.0.101", "https://ms.myDomain.tld"]
}),
logging?: LogOptions
logging: z.object({
level: logLevelSchema.optional(),
file: z.union([logLevelSchema, z.literal(false), fileLogOptionsSchema]).optional(),
console: logLevelSchema.optional(),
}).optional(),
/**
* Disable web server from running/listening on port.
*
* This will also make any ingress sources (Plex, Jellyfin, Tautulli, etc...) unusable
* */
disableWeb?: boolean
disableWeb: z.boolean().optional().meta({
description: "Disable web server from running/listening on port."
}),
/**
* Enables ALL relevant logging and debug options for all sources/clients, when none are defined.
@@ -65,38 +100,45 @@ export interface AIOConfig {
* @default false
* @examples [false]
* */
debugMode?: boolean
debugMode: z.boolean().optional().meta({
description: "Enables ALL relevant logging and debug options for all sources/clients, when none are defined.",
default: false,
examples: [false]
}),
cache?: CacheConfigUser
cache: cacheConfigUserSchema.optional().meta({description: 'Configuration for Caching'}),
transformers?: TransformerCommonConfig[]
transformers: z.array(transformerCommonConfigSchema).optional(),
database?: {
retention?: RetentionConfig<DurationValue>
}
}
database: z.object({
retention: retentionConfigDurationValueSchema.optional(),
}).optional(),
});
export interface AIOClientConfig {
clientDefaults?: RequestRetryOptions
clients?: ClientAIOConfig[]
}
export type AIOConfig = z.infer<typeof aioConfigSchema>;
export interface AIOClientRelaxedConfig {
clientDefaults?: RequestRetryOptions
clients?: object[]
}
export const aioClientRelaxedConfigSchema = z.object({
clientDefaults: requestRetryOptionsSchema.optional(),
clients: z.array(z.looseObject({ type: z.string(), configureAs: z.string().optional() })).optional(),
database: z.object({
retention: retentionConfigDurationValueSchema.optional(),
}).optional(),
});
export interface AIOSourceConfig {
sourceDefaults?: SourceRetryOptions
sources?: SourceAIOConfig[]
}
export type AIOClientRelaxedConfig = z.infer<typeof aioClientRelaxedConfigSchema>;
export interface AIOSourceRelaxedConfig {
sourceDefaults?: SourceRetryOptions
sources?: object[]
}
export const aioSourceRelaxedConfigSchema = z.object({
sourceDefaults: sourceRetryOptionsSchema.optional(),
sources: z.array(z.looseObject({ type: z.string(), configureAs: z.string().optional() })).optional(),
database: z.object({
retention: retentionConfigDurationValueSchema.optional(),
}).optional(),
});
export interface TypedConfig<T = string> {
type: T
// [key: string]: any
}
export type AIOSourceRelaxedConfig = z.infer<typeof aioSourceRelaxedConfigSchema>;
export const typedConfigSchema = z.object({
type: z.string(),
});
export type TypedConfig = z.infer<typeof typedConfigSchema>;

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