diff --git a/config/librefm.json.example b/config/librefm.json.example
new file mode 100644
index 00000000..c6359627
--- /dev/null
+++ b/config/librefm.json.example
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "myLibreFmClient",
+ "enable": true,
+ "configureAs": "client",
+ "data": {
+ "apiKey": "anything",
+ "secret": "anything",
+ "redirectUri": "http://localhost:9078/librefm/callback"
+ }
+ },
+ {
+ "name": "myLibreFmSource",
+ "enable": true,
+ "configureAs": "source",
+ "data": {
+ "apiKey": "anything",
+ "secret": "anything",
+ "redirectUri": "http://localhost:9078/librefm/callback"
+ }
+ }
+]
diff --git a/docsite/docs/configuration/clients/clients.mdx b/docsite/docs/configuration/clients/clients.mdx
index 40b1bdc1..586f4eab 100644
--- a/docsite/docs/configuration/clients/clients.mdx
+++ b/docsite/docs/configuration/clients/clients.mdx
@@ -13,6 +13,7 @@ A **Client** is an application that stores the historical information about what
| Name | Now Playing |
| :-------------------------------------------------- | :---------- |
| [Last.fm](/configuration/clients/lastfm) | ✅ |
+| [Libre.fm](/configuration/clients/librefm) | ✅ |
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
| [Koito](/configuration/clients/koito) | ❌ |
| [Maloja](/configuration/clients/maloja) | ❌ |
diff --git a/docsite/docs/configuration/clients/lastfm.mdx b/docsite/docs/configuration/clients/lastfm.mdx
index 120c3190..29fef12c 100644
--- a/docsite/docs/configuration/clients/lastfm.mdx
+++ b/docsite/docs/configuration/clients/lastfm.mdx
@@ -11,24 +11,46 @@ import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
:::tip[Other Users]
-* To monitor and re-scrobble activity from your Last.fm account create a [Last.fm (Source)](/configuration/sources/lastfm-source)
+* To monitor and re-scrobble activity from your Last.fm/Libre.fm account create a [Last.fm (Source)](/configuration/sources/lastfm-source)
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
:::
-[Register for an API account here.](https://www.last.fm/api/account/create)
+You will need a [Last.fm](https://www.last.fm) account to use the Last.fm API.
+
+After creating your account, [register for an API account here.](https://www.last.fm/api/account/create)
+
+The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callback-redirect-url) section.
+
+**Take note of your API Key and Secret after account creation.**
+
+### Callback/Redirect URL
+
+The Redirect (Callback) URL is used to redirect your **browser** to multi-scrobbler after authentication has completed.
+
+So, the *base of the URL* you use should be the *same* as the URL you would use to navigate to the multi-scrobbler dashboard.
+
+
+
+Example
+
+If you can visit the multi-scrobbler dashboard by navigating to http://192.168.0.100:9078 in your web browser...
+
+then the ENV `LASTFM_REDIRECT_URI`, or `redirectUri` for last.fm/libre.fm json config, should be
-The Callback URL is actually specified by multi-scrobbler but to keep things consistent you should use
```
-http://localhost:9078/lastfm/callback
+http://192.168.0.100:9078/lastfm/callback
```
-or replace `localhost:9078` with your own base URL.
+
+
+
+The Redirect URL can be explicitly specified in the configuration below, or by using the global [Base URL](/configuration#base-url) setting.
## Configuration
| Environmental Variable | Required? | Default | Description |
- |------------------------|-----------|-----------------------------------------|-------------------------------------------------------------------------------|
+ | ---------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
diff --git a/docsite/docs/configuration/clients/librefm.mdx b/docsite/docs/configuration/clients/librefm.mdx
new file mode 100644
index 00000000..ea3c8918
--- /dev/null
+++ b/docsite/docs/configuration/clients/librefm.mdx
@@ -0,0 +1,65 @@
+---
+title: Libre.fm
+toc_min_heading_level: 2
+toc_max_heading_level: 5
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import CodeBlock from '@theme/CodeBlock';
+import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
+
+:::tip[Other Users]
+
+* To monitor and re-scrobble activity from your Libre.fm account create a [Libre.fm (Source)](/configuration/sources/librefm-source)
+
+:::
+
+To use the official [Libre.fm](https://libre.fm/) instance you will need an account. Or, you can run your own Libre.fm instance [using the legacy source code](https://github.com/foocorp/gnu-fm).
+
+No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is needed to use the Libre.fm API. Instead, use **any values** for API Key/Secret in the configuration. Authentication is done by allowing access from your account on redirect.
+
+### Callback/Redirect URL
+
+The Redirect (Callback) URL is used to redirect your **browser** to multi-scrobbler after authentication has completed.
+
+So, the *base of the URL* you use should be the *same* as the URL you would use to navigate to the multi-scrobbler dashboard.
+
+
+
+Example
+
+If you can visit the multi-scrobbler dashboard by navigating to http://192.168.0.100:9078 in your web browser...
+
+then the ENV `LIBREFM_REDIRECT_URI`, or `redirectUri` for libre.fm json config, should be
+
+```
+http://192.168.0.100:9078/librefm/callback
+```
+
+
+
+The Redirect URL can be explicitly specified in the configuration below, or by using the global [Base URL](/configuration#base-url) setting.
+
+### Custom Libre.fm instance
+
+If you are **not** using the official Libre.fm instance then specify your custom instance using ENV `LIBREFM_URLBASE` or `urlBase` property in the `data` section in File/AIO config.
+
+The value should be `protocol://libre.tld/path/prefix` EX:
+
+```
+https://my.cool.libre.com/2.0/
+```
+
+## Configuration
+
+
+ | Environmental Variable | Required? | Default | Description |
+ | ---------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
+ | `LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
+ | `LIBREFM_API_KEY` | No | `anything` | Optional API Key |
+ | `LIBREFM_SECRET` | No | `anything` | Optional shared secret |
+ | `LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
+ | `LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
+ | `LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
+
\ No newline at end of file
diff --git a/docsite/docs/configuration/sources/lastfm-source.mdx b/docsite/docs/configuration/sources/lastfm-source.mdx
index 56c69a26..4a4257de 100644
--- a/docsite/docs/configuration/sources/lastfm-source.mdx
+++ b/docsite/docs/configuration/sources/lastfm-source.mdx
@@ -1,5 +1,5 @@
---
-title: Lastfm
+title: Last.fm
toc_min_heading_level: 2
toc_max_heading_level: 5
---
@@ -13,7 +13,7 @@ This Source **monitors your Last.fm scrobble history** and re-scrobbles new acti
:::tip[Other Uses]
-* To **scrobble to** Last.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
+* To **scrobble to** Last.fm/Libre.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
:::
diff --git a/docsite/docs/configuration/sources/librefm-source.mdx b/docsite/docs/configuration/sources/librefm-source.mdx
new file mode 100644
index 00000000..1eaf1974
--- /dev/null
+++ b/docsite/docs/configuration/sources/librefm-source.mdx
@@ -0,0 +1,28 @@
+---
+title: Libre.fm
+toc_min_heading_level: 2
+toc_max_heading_level: 5
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import CodeBlock from '@theme/CodeBlock';
+import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
+
+This Source **monitors your Libre.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
+
+:::tip[Other Uses]
+
+* To **scrobble to** Libre.fm, create a [Libre.fm (Client)](/configuration/clients/librefm)
+
+:::
+
+To configure this Source use the same registration instructions and configuration data shown in [Libre.fm (Client)](/configuration/clients/librefm) with the difference being `"configureAs": "source"` defined in the File/AIO configuration below.
+
+## Configuration
+
+
+:::note
+ You cannot use ENV variables shown in the [Libre.fm Client config](/configuration/clients/librefm) -- multi-scrobbler assumes Libre.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Libre.fm as a Source.
+:::
+
\ No newline at end of file
diff --git a/docsite/docs/configuration/sources/sources.mdx b/docsite/docs/configuration/sources/sources.mdx
index 3221118f..db7bb015 100644
--- a/docsite/docs/configuration/sources/sources.mdx
+++ b/docsite/docs/configuration/sources/sources.mdx
@@ -23,6 +23,7 @@ A **Source** is a data source that contains information about tracks you are pla
| [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) | ❌ | ✅ | ❌ | ❌ |
diff --git a/docsite/docs/configuration/sources/youtube-music.mdx b/docsite/docs/configuration/sources/youtube-music.mdx
index 00db1291..f1daeec4 100644
--- a/docsite/docs/configuration/sources/youtube-music.mdx
+++ b/docsite/docs/configuration/sources/youtube-music.mdx
@@ -115,7 +115,7 @@ Only one of these methods needs to be used.
If the beginning of the Redirect URI (before `api`) is EXACTLY how you would reach the MS dashboard from your browser (EX `http://localhost:9078`) then edit your google oauth client section for `Authorized redirect URIs` and add the URL MS has displayed.
- If it is NOT EXACTLY the same you either need to set MS's [base url](https://foxxmd.github.io/multi-scrobbler/docs/configuration/#base-url) or you can provide your own (Custom) Redirect URI for MS to use by setting it in `ytmusic.json` or ENV.
+ If it is NOT EXACTLY the same you either need to set MS's [base url](/configuration#base-url) or you can provide your own (Custom) Redirect URI for MS to use by setting it in `ytmusic.json` or ENV.
diff --git a/docsite/docs/index.mdx b/docsite/docs/index.mdx
index fefb9e5e..35c24b94 100644
--- a/docsite/docs/index.mdx
+++ b/docsite/docs/index.mdx
@@ -22,6 +22,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
* [Kodi](/configuration/sources/kodi)
* [Koito](/configuration/sources/koito-source)
* [Last.fm](/configuration/sources/lastfm-source)
+ * [Libre.fm](/configuration/sources/librefm-source)
* [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
* [ListenBrainz](/configuration/sources/listenbrainz-source)
* [ListenBrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
@@ -42,6 +43,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
* Supports scrobbling to many [**Clients**](/configuration/clients)
* [Koito](/configuration/clients/koito)
* [Last.fm](/configuration/clients/lastfm)
+ * [Libre.fm](/configuration/clients/librefm)
* [ListenBrainz](/configuration/clients/listenbrainz)
* [Maloja](/configuration/clients/maloja)
* [Rocksky](/configuration/clients/rocksky)
diff --git a/package-lock.json b/package-lock.json
index b64d3c6d..dc2dae53 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "multi-scrobbler",
- "version": "0.10.7",
+ "version": "0.10.8",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -70,7 +70,7 @@
"json5": "^2.2.3",
"keyv": "^5.5.0",
"kodi-api": "^0.2.1",
- "lastfm-node-client": "^2.2.0",
+ "lastfm-ts-api": "^2.3.0",
"merge-error-cause": "^5.0.2",
"mopidy": "^1.3.0",
"mpd-api": "^1.1.2",
@@ -8294,12 +8294,13 @@
"uuid": "^8.3.2"
}
},
- "node_modules/lastfm-node-client": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/lastfm-node-client/-/lastfm-node-client-2.2.0.tgz",
- "integrity": "sha512-nhHxrRPaNKIJnEuRov68HGjQRAFel2UWnG86ieQAklaVOdBciC6DO0vajZjzPZGqwvUAxQzDHPsp2ceDOtKW2w==",
- "engines": {
- "node": ">=8.10.0"
+ "node_modules/lastfm-ts-api": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/lastfm-ts-api/-/lastfm-ts-api-2.3.0.tgz",
+ "integrity": "sha512-ygLoJS0Hfy89Tv4rd3J3RllEEe5a/JTGdeHNmeKWd1UZNY4B7bY+YsVcinLsbP/yEA4OwqG8jd10Xj8f+KNntg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/scriptex"
}
},
"node_modules/leven": {
diff --git a/package.json b/package.json
index 365cdd23..670cade9 100644
--- a/package.json
+++ b/package.json
@@ -102,7 +102,7 @@
"json5": "^2.2.3",
"keyv": "^5.5.0",
"kodi-api": "^0.2.1",
- "lastfm-node-client": "^2.2.0",
+ "lastfm-ts-api": "^2.3.0",
"merge-error-cause": "^5.0.2",
"mopidy": "^1.3.0",
"mpd-api": "^1.1.2",
diff --git a/patches/lastfm-node-client+2.2.0.patch b/patches/lastfm-node-client+2.2.0.patch
deleted file mode 100644
index 8cdb8f01..00000000
--- a/patches/lastfm-node-client+2.2.0.patch
+++ /dev/null
@@ -1,38 +0,0 @@
-diff --git a/node_modules/lastfm-node-client/lib/ApiRequest.js b/node_modules/lastfm-node-client/lib/ApiRequest.js
-index ecd39d0..c8e1cc5 100644
---- a/node_modules/lastfm-node-client/lib/ApiRequest.js
-+++ b/node_modules/lastfm-node-client/lib/ApiRequest.js
-@@ -91,7 +91,7 @@ class ApiRequest {
- const paramsStr = querystring.stringify(paramsObj);
- const options = {
- hostname: "ws.audioscrobbler.com",
-- path: "/2.0"
-+ path: "/2.0",
- };
-
- if (method === "POST") {
-@@ -113,7 +113,11 @@ class ApiRequest {
- httpResponse.on("data", chunk => data += chunk);
- httpResponse.on("end", () => resolve(data));
- httpResponse.on("error", err => reject(err));
-- });
-+ })
-+ // stop waiting for request if it takes longer than 3 seconds
-+ .setTimeout(3000, () => {
-+ httpRequest.destroy(new Error('ETIMEDOUT - socket hang up'));
-+ });
-
- httpRequest.on("error", err => reject(err));
-
-diff --git a/node_modules/lastfm-node-client/lib/LastFm.js b/node_modules/lastfm-node-client/lib/LastFm.js
-index 2393a4d..75bdf37 100644
---- a/node_modules/lastfm-node-client/lib/LastFm.js
-+++ b/node_modules/lastfm-node-client/lib/LastFm.js
-@@ -1045,7 +1045,6 @@ class LastFm {
- * @param {callback} [callback]
- * @returns {(Promise|LastFm)}
- */
--
- trackUpdateNowPlaying(params, callback) {
- const apiRequest = new ApiRequest()
- .set(params)
diff --git a/src/backend/common/infrastructure/Atomic.ts b/src/backend/common/infrastructure/Atomic.ts
index 281d8cae..58d79376 100644
--- a/src/backend/common/infrastructure/Atomic.ts
+++ b/src/backend/common/infrastructure/Atomic.ts
@@ -13,6 +13,7 @@ export type SourceType =
| 'subsonic'
| 'jellyfin'
| 'lastfm'
+ | 'librefm'
| 'deezer'
| 'endpointlz'
| 'endpointlfm'
@@ -41,6 +42,7 @@ export const sourceTypes: SourceType[] = [
'subsonic',
'jellyfin',
'lastfm',
+ 'librefm',
'deezer',
'endpointlz',
'endpointlfm',
@@ -73,6 +75,7 @@ export const lowGranularitySources: SourceType[] = ['subsonic', 'ytmusic'];
export type ClientType =
'maloja'
| 'lastfm'
+ | 'librefm'
| 'listenbrainz'
| 'koito'
| 'tealfm'
@@ -80,6 +83,7 @@ export type ClientType =
export const clientTypes: ClientType[] = [
'maloja',
'lastfm',
+ 'librefm',
'listenbrainz',
'koito',
'tealfm',
@@ -122,6 +126,8 @@ export interface InternalConfig {
logger: Logger
}
+export type InternalConfigOptional = Omit
+
export type ReportedPlayerStatus = 'playing' | 'stopped' | 'paused' | 'unknown';
export const REPORTED_PLAYER_STATUSES = {
playing: 'playing' as ReportedPlayerStatus,
diff --git a/src/backend/common/infrastructure/config/client/clients.ts b/src/backend/common/infrastructure/config/client/clients.ts
index 574dbc89..4dbcd1bb 100644
--- a/src/backend/common/infrastructure/config/client/clients.ts
+++ b/src/backend/common/infrastructure/config/client/clients.ts
@@ -4,7 +4,8 @@ import { ListenBrainzClientAIOConfig, ListenBrainzClientConfig } from "./listenb
import { MalojaClientAIOConfig, MalojaClientConfig } from "./maloja.js";
import { TealClientAIOConfig, TealClientConfig } from "./tealfm.js";
import { RockSkyClientAIOConfig, RockSkyClientConfig } from "./rocksky.js";
+import { LibrefmClientConfig, LibrefmClientAIOConfig } from "./librefm.js";
-export type ClientConfig = MalojaClientConfig | LastfmClientConfig | ListenBrainzClientConfig | KoitoClientConfig | TealClientConfig | RockSkyClientConfig;
+export type ClientConfig = MalojaClientConfig | LastfmClientConfig | LibrefmClientConfig | ListenBrainzClientConfig | KoitoClientConfig | TealClientConfig | RockSkyClientConfig;
-export type ClientAIOConfig = MalojaClientAIOConfig | LastfmClientAIOConfig | ListenBrainzClientAIOConfig | KoitoClientAIOConfig | TealClientAIOConfig | RockSkyClientAIOConfig;
+export type ClientAIOConfig = MalojaClientAIOConfig | LastfmClientAIOConfig | LibrefmClientAIOConfig | ListenBrainzClientAIOConfig | KoitoClientAIOConfig | TealClientAIOConfig | RockSkyClientAIOConfig;
diff --git a/src/backend/common/infrastructure/config/client/lastfm.ts b/src/backend/common/infrastructure/config/client/lastfm.ts
index 305dcc59..db664e6c 100644
--- a/src/backend/common/infrastructure/config/client/lastfm.ts
+++ b/src/backend/common/infrastructure/config/client/lastfm.ts
@@ -3,13 +3,13 @@ import { CommonClientConfig, CommonClientData, CommonClientOptions, NowPlayingOp
export interface LastfmData extends CommonClientData, RequestRetryOptions {
/**
- * API Key generated from Last.fm account
+ * API Key generated from Last.fm/Libre.fm account
*
* @examples ["787c921a2a2ab42320831aba0c8f2fc2"]
* */
apiKey: string
/**
- * Secret generated from Last.fm account
+ * Secret generated from Last.fm/Libre.fm account
*
* @examples ["ec42e09d5ae0ee0f0816ca151008412a"]
* */
diff --git a/src/backend/common/infrastructure/config/client/librefm.ts b/src/backend/common/infrastructure/config/client/librefm.ts
new file mode 100644
index 00000000..0c0b4fbf
--- /dev/null
+++ b/src/backend/common/infrastructure/config/client/librefm.ts
@@ -0,0 +1,63 @@
+import { RequestRetryOptions } from "../common.js";
+import { CommonClientConfig, CommonClientData } from "./index.js";
+import { LastfmClientOptions } from "./lastfm.js";
+
+export interface LibrefmData extends CommonClientData, RequestRetryOptions {
+ /**
+ * Optional Secret for Libre.fm account
+ *
+ * This can be anything, or it will be generated by multi-scrobbler
+ *
+ * @examples ["anyValue"]
+ * */
+ apiKey?: string
+ /**
+ * Optional Secret for Libre.fm account
+ *
+ * This can be anything, or it will be generated by multi-scrobbler
+ *
+ * @examples ["anyValue"]
+ * */
+ secret?: string
+
+ /**
+ * Optional session id returned from a completed auth flow
+ * */
+ session?: string
+
+ path?: string
+
+ /**
+ * (Optional) The host and path prefix for your Libre.fm instance
+ *
+ * @default 'https://libre.fm/2.0/'
+ */
+ urlBase?: string
+ /**
+ * Optional URI to use for callback. Specify this if callback should be different than the default. MUST have "librefm/callback" in the URL somewhere.
+ *
+ * @default "http://localhost:9078/librefm/callback"
+ * @examples ["http://localhost:9078/librefm/callback"]
+ * */
+ redirectUri?: string
+}
+
+export interface LibrefmClientOptions extends LastfmClientOptions {
+
+}
+
+export interface LibrefmClientConfig extends CommonClientConfig {
+ /**
+ * Should always be `client` when using Librefm as a client
+ *
+ * @default client
+ * @examples ["client"]
+ * */
+ configureAs?: 'client' | 'source'
+ data: LibrefmData
+ options?: LibrefmClientOptions
+}
+
+export interface LibrefmClientAIOConfig extends LibrefmClientConfig {
+ type: 'librefm'
+}
\ No newline at end of file
diff --git a/src/backend/common/infrastructure/config/source/librefm.ts b/src/backend/common/infrastructure/config/source/librefm.ts
new file mode 100644
index 00000000..cc67cb87
--- /dev/null
+++ b/src/backend/common/infrastructure/config/source/librefm.ts
@@ -0,0 +1,20 @@
+import { LibrefmData } from "../client/librefm.js";
+import { PollingOptions } from "../common.js";
+import { CommonSourceConfig, CommonSourceData } from "./index.js";
+
+export interface librefmSourceData extends CommonSourceData, PollingOptions, LibrefmData{}
+
+export interface LibrefmSourceConfig extends CommonSourceConfig {
+ /**
+ * When used in `librefm.config` this tells multi-scrobbler whether to use this data to configure a source or client.
+ *
+ * @default source
+ * @examples ["source"]
+ * */
+ configureAs?: 'source'
+ data: LibrefmData
+}
+
+export interface LibrefmSouceAIOConfig extends LibrefmSourceConfig {
+ type: 'librefm'
+}
diff --git a/src/backend/common/infrastructure/config/source/sources.ts b/src/backend/common/infrastructure/config/source/sources.ts
index a8855207..ccc05d80 100644
--- a/src/backend/common/infrastructure/config/source/sources.ts
+++ b/src/backend/common/infrastructure/config/source/sources.ts
@@ -24,6 +24,7 @@ import { KoitoSourceAIOConfig, KoitoSourceConfig } from "./koito.js";
import { MalojaSourceAIOConfig, MalojaSourceConfig } from "./maloja.js";
import { TealSourceAIOConfig, TealSourceConfig } from "./tealfm.js";
import { RockskySourceAIOConfig, RockskySourceConfig } from "./rocksky.js";
+import { LibrefmSouceAIOConfig, LibrefmSourceConfig } from "./librefm.js";
export type SourceConfig =
@@ -36,6 +37,7 @@ export type SourceConfig =
| SubSonicSourceConfig
| JellyApiSourceConfig
| LastfmSourceConfig
+ | LibrefmSourceConfig
| YTMusicSourceConfig
| MPRISSourceConfig
| MopidySourceConfig
@@ -65,6 +67,7 @@ export type SourceAIOConfig =
| SubsonicSourceAIOConfig
| JellyApiSourceAIOConfig
| LastFmSouceAIOConfig
+ | LibrefmSouceAIOConfig
| YTMusicSourceAIOConfig
| MPRISSourceAIOConfig
| MopidySourceAIOConfig
diff --git a/src/backend/common/infrastructure/typings/lastfm-node-client.d.ts b/src/backend/common/infrastructure/typings/lastfm-node-client.d.ts
deleted file mode 100644
index 1f9d2839..00000000
--- a/src/backend/common/infrastructure/typings/lastfm-node-client.d.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-declare module 'lastfm-node-client' {
- /**
- * Must REMOVE (unset) any undefined/null properties from payload before sending or LastFM will return an error
- * */
- export interface TrackScrobblePayload {
- /**
- * join multiple artists with ', '
- * */
- artist: string
-
- /**
- * track title
- * */
- track: string
-
- /**
- * Unix timestamp of time track should be scrobbled at
- * */
- timestamp: number
- /**
- * length of track in seconds
- * */
- duration?: number
-
- album?: string
-
- albumArtist?: string
-
- /** MusicBrainz track ID */
- mbid?: string
- }
-
- export interface TrackScrobbleRequest extends TrackScrobblePayload {
- method: 'track.scrobble'
- }
-
- export interface TrackScrobbleResponse {
- scrobbles: {
- '@attr': {
- accepted: number,
- ignored: number
- code: number
- },
- scrobble?: {
- track: {
- '#text': string
- },
- timestamp: number,
- ignoredMessage: {
- code: number
- '#text': string
- }
- }
- }
- }
-
- export type NowPlayingPayload = Omit
-
- export type NowPlayingRequest = NowPlayingPayload & { method: 'track.updateNowPlaying' };
-
- export type LastfmTrackUpdateRequest = TrackScrobbleRequest | NowPlayingRequest;
-
- export interface NowPlayingResponse {
- nowplaying: {
- ignoredMessage: {
- code: number
- '#text': string
- }
- }
- }
-
- export interface AuthGetSessionPayload {
- token: string
- }
-
- export interface AuthGetSessionResponse {
- session: {
- key: string
- name: string
- }
- }
-
- export interface UserGetInfoResponse {
- user: {
- name: string
- }
- }
-
- export interface UserGetRecentTracksPayload {
- user: string
- limit?: number
- extended?: boolean
- }
-
- export interface UserGetRecentTracksResponse {
- recenttracks: {
- track: TrackObject[]
- }
- }
-
- export interface TrackObject {
- artist: {
- '#text'?: string,
- name: string,
- mbid: string,
- },
- name: string,
- album: {
- '#text': string,
- mbid: string
- },
- duration?: number,
- date?: {
- uts: number | string,
- '#text': string
- },
- '@attr'?: {
- nowplaying: 'true' | 'false'
- }
- url: string,
- mbid: string,
- }
-
- export default class LastFM {
- constructor(apiKey: string, secret?: string, session?: string);
-
- trackUpdateNowPlaying(params: NowPlayingPayload): Promise
- trackScrobble(params: TrackScrobblePayload): Promise
- authGetSession(params: AuthGetSessionPayload): Promise
- userGetInfo(): Promise
- userGetRecentTracks(params: UserGetRecentTracksPayload): Promise
- }
-}
diff --git a/src/backend/common/schema/aio-client.json b/src/backend/common/schema/aio-client.json
index f7188e6a..b8de6cc1 100644
--- a/src/backend/common/schema/aio-client.json
+++ b/src/backend/common/schema/aio-client.json
@@ -1 +1 @@
-{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"clientDefaults":{"$ref":"#/definitions/RequestRetryOptions"},"clients":{"type":"array","items":{"$ref":"#/definitions/ClientAIOConfig"}}},"definitions":{"RequestRetryOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]}},"title":"RequestRetryOptions"},"ClientAIOConfig":{"anyOf":[{"$ref":"#/definitions/MalojaClientAIOConfig"},{"$ref":"#/definitions/LastfmClientAIOConfig"},{"$ref":"#/definitions/ListenBrainzClientAIOConfig"},{"$ref":"#/definitions/KoitoClientAIOConfig"},{"$ref":"#/definitions/TealClientAIOConfig"},{"$ref":"#/definitions/RockSkyClientAIOConfig"}],"title":"ClientAIOConfig"},"MalojaClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","name","type"],"title":"MalojaClientAIOConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3CSearchAndReplaceTerm%3E"},{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"}],"title":"PlayTransformPartsConfig"},"PlayTransformPartsArray":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"},"title":"PlayTransformPartsArray"},"PlayTransformParts":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"artists":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"album":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}}},"title":"PlayTransformParts"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"album":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{},"replace":{}},"required":["search","replace"],"title":"ConditionalSearchAndReplaceTerm"},"LastfmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","name","type"],"title":"LastfmClientAIOConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"ListenBrainzClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","name","type"],"title":"ListenBrainzClientAIOConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["koito"]}},"required":["data","name","type"],"title":"KoitoClientAIOConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","name","type"],"title":"TealClientAIOConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","name","type"],"title":"RockSkyClientAIOConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"}}}
\ No newline at end of file
+{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"clientDefaults":{"$ref":"#/definitions/RequestRetryOptions"},"clients":{"type":"array","items":{"$ref":"#/definitions/ClientAIOConfig"}}},"definitions":{"RequestRetryOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]}},"title":"RequestRetryOptions"},"ClientAIOConfig":{"anyOf":[{"$ref":"#/definitions/MalojaClientAIOConfig"},{"$ref":"#/definitions/LastfmClientAIOConfig"},{"$ref":"#/definitions/ListenBrainzClientAIOConfig"},{"$ref":"#/definitions/KoitoClientAIOConfig"},{"$ref":"#/definitions/TealClientAIOConfig"},{"$ref":"#/definitions/RockSkyClientAIOConfig"}],"title":"ClientAIOConfig"},"MalojaClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","name","type"],"title":"MalojaClientAIOConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"}],"description":"Represents the weakly-defined user config. May be an array of parts or one parts object","title":"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>"},"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},"title":"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"anyOf":[{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeMetadata"},"name":{"type":"string"},"stageHash":{"type":"string"},"score":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeUser"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string","enum":["native"]},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"name":{"type":"string"}}},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{},"artists":{},"albumArtists":{},"album":{},"duration":{},"meta":{},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]}],"title":"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"albumArtists":{"type":"string"},"album":{"type":"string"},"duration":{"type":"string"},"meta":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"Whennable":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"}},"title":"Whennable"},"FlowControlTerm":{"type":"string","enum":["continue","stop"],"title":"FlowControlTerm"},"StageTypeMetadata":{"type":"string","enum":["spotify","musicbrainz","native"],"title":"StageTypeMetadata"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"$ref":"#/definitions/ConditionalSearchAndReplaceRegExp","title":"ConditionalSearchAndReplaceTerm"},"ConditionalSearchAndReplaceRegExp":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["replace","search"],"title":"ConditionalSearchAndReplaceRegExp"},"SearchAndReplaceRegExp":{"type":"object","properties":{"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["search","replace"],"title":"SearchAndReplaceRegExp"},"ExternalMetadataTerm":{"anyOf":[{"type":"boolean"},{"not":{}},{"$ref":"#/definitions/Whennable"}],"title":"ExternalMetadataTerm"},"StageTypeUser":{"type":"string","enum":["user"],"title":"StageTypeUser"},"LastfmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","name","type"],"title":"LastfmClientAIOConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"ListenBrainzClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","name","type"],"title":"ListenBrainzClientAIOConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["koito"]}},"required":["data","name","type"],"title":"KoitoClientAIOConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","name","type"],"title":"TealClientAIOConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","name","type"],"title":"RockSkyClientAIOConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"}}}
\ No newline at end of file
diff --git a/src/backend/common/schema/aio-source.json b/src/backend/common/schema/aio-source.json
index de2be93a..660b5a48 100644
--- a/src/backend/common/schema/aio-source.json
+++ b/src/backend/common/schema/aio-source.json
@@ -1 +1 @@
-{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sourceDefaults":{"$ref":"#/definitions/SourceRetryOptions"},"sources":{"type":"array","items":{"$ref":"#/definitions/SourceAIOConfig"}}},"definitions":{"SourceRetryOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]}},"title":"SourceRetryOptions"},"SourceAIOConfig":{"anyOf":[{"$ref":"#/definitions/SpotifySourceAIOConfig"},{"$ref":"#/definitions/PlexSourceAIOConfig"},{"$ref":"#/definitions/PlexApiSourceAIOConfig"},{"$ref":"#/definitions/TautulliSourceAIOConfig"},{"$ref":"#/definitions/DeezerSourceAIOConfig"},{"$ref":"#/definitions/DeezerInternalAIOConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceAIOConfig"},{"$ref":"#/definitions/LastFMEndpointSourceAIOConfig"},{"$ref":"#/definitions/SubsonicSourceAIOConfig"},{"$ref":"#/definitions/JellySourceAIOConfig"},{"$ref":"#/definitions/JellyApiSourceAIOConfig"},{"$ref":"#/definitions/LastFmSouceAIOConfig"},{"$ref":"#/definitions/YTMusicSourceAIOConfig"},{"$ref":"#/definitions/MPRISSourceAIOConfig"},{"$ref":"#/definitions/MopidySourceAIOConfig"},{"$ref":"#/definitions/ListenBrainzSourceAIOConfig"},{"$ref":"#/definitions/JRiverSourceAIOConfig"},{"$ref":"#/definitions/KodiSourceAIOConfig"},{"$ref":"#/definitions/WebScrobblerSourceAIOConfig"},{"$ref":"#/definitions/ChromecastSourceAIOConfig"},{"$ref":"#/definitions/MalojaSourceAIOConfig"},{"$ref":"#/definitions/MusikcubeSourceAIOConfig"},{"$ref":"#/definitions/MusicCastSourceAIOConfig"},{"$ref":"#/definitions/MPDSourceAIOConfig"},{"$ref":"#/definitions/VLCSourceAIOConfig"},{"$ref":"#/definitions/IcecastSourceAIOConfig"},{"$ref":"#/definitions/AzuracastSourceAIOConfig"},{"$ref":"#/definitions/KoitoSourceAIOConfig"},{"$ref":"#/definitions/TealSourceAIOConfig"},{"$ref":"#/definitions/RockskySourceAIOConfig"}],"title":"SourceAIOConfig"},"SpotifySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["spotify"]}},"required":["data","type"],"title":"SpotifySourceAIOConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\n\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\n\nReading:\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\n * Rate limit may ~180 req/min\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug"],"description":"Names of log levels that can be invoked on the logger\n\nFrom lowest to highest:\n\n* `debug`\n* `verbose`\n* `log`\n* `info`\n* `warn`\n* `error`\n* `fatal`\n* `silent` (will never output anything)\n\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\n\nWhen\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\n* `frequency` is defined\n\nThis determines the format of the datetime inserted into the log file name:\n\n* `unix` - unix epoch timestamp in milliseconds\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'\n* `auto`\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\n\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\n\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\n\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\n\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\n\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\n\nMay also be specified using env LOG_PATH or a function that returns a string.\n\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\n\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\n\nSet to null to disable.\n\nNOTE: This should be used with care when the Source is a \"polling\" type (has an 'interval' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \"seen\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3CSearchAndReplaceTerm%3E"},{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"}],"title":"PlayTransformPartsConfig"},"PlayTransformPartsArray":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"},"title":"PlayTransformPartsArray"},"PlayTransformParts":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"artists":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"album":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}}},"title":"PlayTransformParts"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"album":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{},"replace":{}},"required":["search","replace"],"title":"ConditionalSearchAndReplaceTerm"},"PlexSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","type"],"title":"PlexSourceAIOConfig"},"PlexSourceData":{"type":"object","properties":{"user":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of users to scrobble tracks from\n\nIf none are provided tracks from all users will be scrobbled","examples":[["MyUser1","MyUser2"]]},"libraries":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of libraries to scrobble tracks from\n\nIf none are provided tracks from all libraries will be scrobbled","examples":[["Audio","Music"]]},"servers":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of servers to scrobble tracks from\n\nIf none are provided tracks from all servers will be scrobbled","examples":[["MyServerName"]]}},"title":"PlexSourceData"},"PlexApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","options","type"],"title":"PlexApiSourceAIOConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\n\nUseful for Plex servers using \"Required\" Secure Connections with self-signed certificates\n\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"TautulliSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["tautulli"]}},"required":["data","type"],"title":"TautulliSourceAIOConfig"},"DeezerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerSourceAIOConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerInternalAIOConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlz"]}},"required":["type"],"title":"ListenbrainzEndpointSourceAIOConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/listenbrainz/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/listenbrainz/originb\n\nIf no slug is found from an extension's incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\n\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlfm"]}},"required":["type"],"title":"LastFMEndpointSourceAIOConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/lastfm/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/lastfm/originb\n\nIf no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubsonicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["subsonic"]}},"required":["data","type"],"title":"SubsonicSourceAIOConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\n\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \"Authentication\" section here => https://www.subsonic.org/pages/api.jsp\n\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \"Token authentication not supported for LDAP users.\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","type"],"title":"JellySourceAIOConfig"},"JellyData":{"type":"object","properties":{"users":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of users to scrobble tracks from\n\nIf none are provided tracks from all users will be scrobbled","examples":[["MyUser1","MyUser2"]]},"servers":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of servers to scrobble tracks from\n\nIf none are provided tracks from all servers will be scrobbled","examples":[["MyServerName1"]]}},"title":"JellyData"},"JellyApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","options","type"],"title":"JellyApiSourceAIOConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\n\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\n\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than 'music'\n\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \"Unknown\" to be counted as Audio","default":false},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\nIf unspecified it will use the normal server HOST and PORT from the `url`\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\n\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastFmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","type"],"title":"LastFmSouceAIOConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"YTMusicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\n\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}},"type":{"type":"string","enum":["ytmusic"]}},"required":["type"],"title":"YTMusicSourceAIOConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\n\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\n\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\n\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\n\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\n\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\n\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"MPRISSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mpris"]}},"required":["data","type"],"title":"MPRISSourceAIOConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mopidy"]}},"required":["data","type"],"title":"MopidySourceAIOConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\n\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\n\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `6680`\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Don't scrobble tracks from soundcloud by adding 'soundcloud' to this list.\n\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Only scrobble tracks from soundcloud by adding 'soundcloud' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\n\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \"Soundcloud\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","type"],"title":"ListenBrainzSourceAIOConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jriver"]}},"required":["data","type"],"title":"JRiverSourceAIOConfig"},"JRiverData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `52199`\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["kodi"]}},"required":["data","type"],"title":"KodiSourceAIOConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["webscrobbler"]}},"required":["type"],"title":"WebScrobblerSourceAIOConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIn WebScrobbler's Webhook you must set an 'API URL'. All MS WebScrobbler sources must start like:\n\nhttp://localhost:9078/api/webscrobbler\n\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/webscrobbler/usera\n* slug: 'userb' => API URL: http://localhost:9078/api/webscrobbler/userb\n\nIf no slug is found from an extension's incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["chromecast"]}},"required":["data","type"],"title":"ChromecastSourceAIOConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\n\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\n\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\n\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \"media type\" in the status info returned for whatever is currently playing\n\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\n\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \"metadata\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \"media type\" reported\n\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","type"],"title":"MalojaSourceAIOConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musikcube"]}},"required":["data","type"],"title":"MusikcubeSourceAIOConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\n\nYou MUST have enabled 'metadata' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\n * musikcube -> settings -> server setup\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\n\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musiccast"]}},"required":["data","type"],"title":"MusicCastSourceAIOConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"},"type":{"type":"string","enum":["mpd"]}},"required":["data","options","type"],"title":"MPDSourceAIOConfig"},"MPDData":{"type":"object","properties":{"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\n\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\n\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"},"type":{"type":"string","enum":["vlc"]}},"required":["data","type"],"title":"VLCSourceAIOConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\n\nTo use this you must have the Web (http) interface module enabled and a password set https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\n\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\n\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"},"type":{"type":"string","enum":["icecast"]}},"required":["data","type"],"title":"IcecastSourceAIOConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\n\nIf not specified MS will use a Source's specific behavior, see Source's documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["azuracast"]}},"required":["data","type"],"title":"AzuracastSourceAIOConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\n\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\n\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\n\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\n\n* `true` => has any current listeners\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\n\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["koito"]}},"required":["data","type"],"title":"KoitoSourceAIOConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","type"],"title":"TealSourceAIOConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","type"],"title":"RockskySourceAIOConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"}}}
\ No newline at end of file
+{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sourceDefaults":{"$ref":"#/definitions/SourceRetryOptions"},"sources":{"type":"array","items":{"$ref":"#/definitions/SourceAIOConfig"}}},"definitions":{"SourceRetryOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]}},"title":"SourceRetryOptions"},"SourceAIOConfig":{"anyOf":[{"$ref":"#/definitions/SpotifySourceAIOConfig"},{"$ref":"#/definitions/PlexApiSourceAIOConfig"},{"$ref":"#/definitions/DeezerSourceAIOConfig"},{"$ref":"#/definitions/DeezerInternalAIOConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceAIOConfig"},{"$ref":"#/definitions/LastFMEndpointSourceAIOConfig"},{"$ref":"#/definitions/SubsonicSourceAIOConfig"},{"$ref":"#/definitions/JellyApiSourceAIOConfig"},{"$ref":"#/definitions/LastFmSouceAIOConfig"},{"$ref":"#/definitions/YTMusicSourceAIOConfig"},{"$ref":"#/definitions/MPRISSourceAIOConfig"},{"$ref":"#/definitions/MopidySourceAIOConfig"},{"$ref":"#/definitions/ListenBrainzSourceAIOConfig"},{"$ref":"#/definitions/JRiverSourceAIOConfig"},{"$ref":"#/definitions/KodiSourceAIOConfig"},{"$ref":"#/definitions/WebScrobblerSourceAIOConfig"},{"$ref":"#/definitions/ChromecastSourceAIOConfig"},{"$ref":"#/definitions/MalojaSourceAIOConfig"},{"$ref":"#/definitions/MusikcubeSourceAIOConfig"},{"$ref":"#/definitions/MusicCastSourceAIOConfig"},{"$ref":"#/definitions/MPDSourceAIOConfig"},{"$ref":"#/definitions/VLCSourceAIOConfig"},{"$ref":"#/definitions/IcecastSourceAIOConfig"},{"$ref":"#/definitions/AzuracastSourceAIOConfig"},{"$ref":"#/definitions/KoitoSourceAIOConfig"},{"$ref":"#/definitions/TealSourceAIOConfig"},{"$ref":"#/definitions/RockskySourceAIOConfig"}],"title":"SourceAIOConfig"},"SpotifySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["spotify"]}},"required":["data","type"],"title":"SpotifySourceAIOConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\n\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\n\nReading:\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\n * Rate limit may ~180 req/min\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug"],"description":"Names of log levels that can be invoked on the logger\n\nFrom lowest to highest:\n\n* `debug`\n* `verbose`\n* `log`\n* `info`\n* `warn`\n* `error`\n* `fatal`\n* `silent` (will never output anything)\n\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\n\nWhen\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\n* `frequency` is defined\n\nThis determines the format of the datetime inserted into the log file name:\n\n* `unix` - unix epoch timestamp in milliseconds\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'\n* `auto`\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\n\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\n\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\n\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\n\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\n\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\n\nMay also be specified using env LOG_PATH or a function that returns a string.\n\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\n\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\n\nSet to null to disable.\n\nNOTE: This should be used with care when the Source is a \"polling\" type (has an 'interval' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \"seen\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"}],"description":"Represents the weakly-defined user config. May be an array of parts or one parts object","title":"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>"},"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},"title":"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"anyOf":[{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeMetadata"},"name":{"type":"string"},"stageHash":{"type":"string"},"score":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeUser"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string","enum":["native"]},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"name":{"type":"string"}}},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{},"artists":{},"albumArtists":{},"album":{},"duration":{},"meta":{},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]}],"title":"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"albumArtists":{"type":"string"},"album":{"type":"string"},"duration":{"type":"string"},"meta":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"Whennable":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"}},"title":"Whennable"},"FlowControlTerm":{"type":"string","enum":["continue","stop"],"title":"FlowControlTerm"},"StageTypeMetadata":{"type":"string","enum":["spotify","musicbrainz","native"],"title":"StageTypeMetadata"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"$ref":"#/definitions/ConditionalSearchAndReplaceRegExp","title":"ConditionalSearchAndReplaceTerm"},"ConditionalSearchAndReplaceRegExp":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["replace","search"],"title":"ConditionalSearchAndReplaceRegExp"},"SearchAndReplaceRegExp":{"type":"object","properties":{"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["search","replace"],"title":"SearchAndReplaceRegExp"},"ExternalMetadataTerm":{"anyOf":[{"type":"boolean"},{"not":{}},{"$ref":"#/definitions/Whennable"}],"title":"ExternalMetadataTerm"},"StageTypeUser":{"type":"string","enum":["user"],"title":"StageTypeUser"},"PlexApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","type"],"title":"PlexApiSourceAIOConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\n\nUseful for Plex servers using \"Required\" Secure Connections with self-signed certificates\n\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"DeezerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerSourceAIOConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerInternalAIOConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlz"]}},"required":["type"],"title":"ListenbrainzEndpointSourceAIOConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/listenbrainz/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/listenbrainz/originb\n\nIf no slug is found from an extension's incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\n\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlfm"]}},"required":["type"],"title":"LastFMEndpointSourceAIOConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/lastfm/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/lastfm/originb\n\nIf no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubsonicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["subsonic"]}},"required":["data","type"],"title":"SubsonicSourceAIOConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\n\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \"Authentication\" section here => https://www.subsonic.org/pages/api.jsp\n\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \"Token authentication not supported for LDAP users.\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellyApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","options","type"],"title":"JellyApiSourceAIOConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\n\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\n\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than 'music'\n\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \"Unknown\" to be counted as Audio","default":false},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\nIf unspecified it will use the normal server HOST and PORT from the `url`\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\n\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastFmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","type"],"title":"LastFmSouceAIOConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"YTMusicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\n\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}},"type":{"type":"string","enum":["ytmusic"]}},"required":["type"],"title":"YTMusicSourceAIOConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\n\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\n\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\n\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\n\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\n\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\n\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"MPRISSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mpris"]}},"required":["data","type"],"title":"MPRISSourceAIOConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mopidy"]}},"required":["data","type"],"title":"MopidySourceAIOConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\n\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\n\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `6680`\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Don't scrobble tracks from soundcloud by adding 'soundcloud' to this list.\n\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Only scrobble tracks from soundcloud by adding 'soundcloud' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\n\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \"Soundcloud\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","type"],"title":"ListenBrainzSourceAIOConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jriver"]}},"required":["data","type"],"title":"JRiverSourceAIOConfig"},"JRiverData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `52199`\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["kodi"]}},"required":["data","type"],"title":"KodiSourceAIOConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["webscrobbler"]}},"required":["type"],"title":"WebScrobblerSourceAIOConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIn WebScrobbler's Webhook you must set an 'API URL'. All MS WebScrobbler sources must start like:\n\nhttp://localhost:9078/api/webscrobbler\n\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/webscrobbler/usera\n* slug: 'userb' => API URL: http://localhost:9078/api/webscrobbler/userb\n\nIf no slug is found from an extension's incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["chromecast"]}},"required":["data","type"],"title":"ChromecastSourceAIOConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\n\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\n\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\n\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \"media type\" in the status info returned for whatever is currently playing\n\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\n\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \"metadata\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \"media type\" reported\n\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","type"],"title":"MalojaSourceAIOConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musikcube"]}},"required":["data","type"],"title":"MusikcubeSourceAIOConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\n\nYou MUST have enabled 'metadata' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\n * musikcube -> settings -> server setup\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\n\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musiccast"]}},"required":["data","type"],"title":"MusicCastSourceAIOConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"},"type":{"type":"string","enum":["mpd"]}},"required":["data","options","type"],"title":"MPDSourceAIOConfig"},"MPDData":{"type":"object","properties":{"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\n\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\n\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"},"type":{"type":"string","enum":["vlc"]}},"required":["data","type"],"title":"VLCSourceAIOConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\n\nTo use this you must have the Web (http) interface module enabled and a password set https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\n\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\n\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"},"type":{"type":"string","enum":["icecast"]}},"required":["data","type"],"title":"IcecastSourceAIOConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\n\nIf not specified MS will use a Source's specific behavior, see Source's documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["azuracast"]}},"required":["data","type"],"title":"AzuracastSourceAIOConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\n\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\n\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\n\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\n\n* `true` => has any current listeners\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\n\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["koito"]}},"required":["data","type"],"title":"KoitoSourceAIOConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","type"],"title":"TealSourceAIOConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"},"serviceAllow":{"type":"array","items":{"type":"string"}},"serviceDeny":{"type":"array","items":{"type":"string"}}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","type"],"title":"RockskySourceAIOConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"}}}
\ No newline at end of file
diff --git a/src/backend/common/schema/aio.json b/src/backend/common/schema/aio.json
index 36b2296b..d8f05af2 100644
--- a/src/backend/common/schema/aio.json
+++ b/src/backend/common/schema/aio.json
@@ -1 +1 @@
-{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sourceDefaults":{"$ref":"#/definitions/SourceDefaults"},"clientDefaults":{"$ref":"#/definitions/ClientDefaults"},"sources":{"type":"array","items":{"$ref":"#/definitions/SourceAIOConfig"}},"clients":{"type":"array","items":{"$ref":"#/definitions/ClientAIOConfig"}},"webhooks":{"type":"array","items":{"$ref":"#/definitions/WebhookConfig"}},"port":{"type":"number","description":"Set the port the multi-scrobbler UI will be served from","default":9078,"examples":[9078]},"baseUrl":{"type":"string","description":"Set the Base URL the application should assume the UI is served from.\n\nThis will affect how default redirect URLs are generated (spotify, lastfm, deezer) and some logging messages.\n\nIt will NOT set the actual interface/IP that the application is listening on.\n\nThis can also be set using the BASE_URL environmental variable.","default":"http://localhost","examples":["http://localhost","http://192.168.0.101","https://ms.myDomain.tld"]},"logging":{"$ref":"#/definitions/LogOptions"},"disableWeb":{"type":"boolean","description":"Disable web server from running/listening on port.\n\nThis will also make any ingress sources (Plex, Jellyfin, Tautulli, etc...) unusable"},"debugMode":{"type":"boolean","description":"Enables ALL relevant logging and debug options for all sources/clients, when none are defined.\n\nThis is a convenience shortcut for enabling all output needed to troubleshoot an issue and does not need to be on for normal operation.\n\nIt can also be enabled with the environmental variable DEBUG_MODE=true","default":false,"examples":[false]},"cache":{"$ref":"#/definitions/CacheConfigOptions"}},"definitions":{"SourceDefaults":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"SourceDefaults"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug"],"description":"Names of log levels that can be invoked on the logger\n\nFrom lowest to highest:\n\n* `debug`\n* `verbose`\n* `log`\n* `info`\n* `warn`\n* `error`\n* `fatal`\n* `silent` (will never output anything)\n\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\n\nWhen\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\n* `frequency` is defined\n\nThis determines the format of the datetime inserted into the log file name:\n\n* `unix` - unix epoch timestamp in milliseconds\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'\n* `auto`\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\n\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\n\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\n\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\n\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\n\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\n\nMay also be specified using env LOG_PATH or a function that returns a string.\n\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\n\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\n\nSet to null to disable.\n\nNOTE: This should be used with care when the Source is a \"polling\" type (has an 'interval' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \"seen\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3CSearchAndReplaceTerm%3E"},{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"}],"title":"PlayTransformPartsConfig"},"PlayTransformPartsArray":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"},"title":"PlayTransformPartsArray"},"PlayTransformParts":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"artists":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"album":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}}},"title":"PlayTransformParts"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"album":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{},"replace":{}},"required":["search","replace"],"title":"ConditionalSearchAndReplaceTerm"},"ClientDefaults":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"ClientDefaults"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"SourceAIOConfig":{"anyOf":[{"$ref":"#/definitions/SpotifySourceAIOConfig"},{"$ref":"#/definitions/PlexSourceAIOConfig"},{"$ref":"#/definitions/PlexApiSourceAIOConfig"},{"$ref":"#/definitions/TautulliSourceAIOConfig"},{"$ref":"#/definitions/DeezerSourceAIOConfig"},{"$ref":"#/definitions/DeezerInternalAIOConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceAIOConfig"},{"$ref":"#/definitions/LastFMEndpointSourceAIOConfig"},{"$ref":"#/definitions/SubsonicSourceAIOConfig"},{"$ref":"#/definitions/JellySourceAIOConfig"},{"$ref":"#/definitions/JellyApiSourceAIOConfig"},{"$ref":"#/definitions/LastFmSouceAIOConfig"},{"$ref":"#/definitions/YTMusicSourceAIOConfig"},{"$ref":"#/definitions/MPRISSourceAIOConfig"},{"$ref":"#/definitions/MopidySourceAIOConfig"},{"$ref":"#/definitions/ListenBrainzSourceAIOConfig"},{"$ref":"#/definitions/JRiverSourceAIOConfig"},{"$ref":"#/definitions/KodiSourceAIOConfig"},{"$ref":"#/definitions/WebScrobblerSourceAIOConfig"},{"$ref":"#/definitions/ChromecastSourceAIOConfig"},{"$ref":"#/definitions/MalojaSourceAIOConfig"},{"$ref":"#/definitions/MusikcubeSourceAIOConfig"},{"$ref":"#/definitions/MusicCastSourceAIOConfig"},{"$ref":"#/definitions/MPDSourceAIOConfig"},{"$ref":"#/definitions/VLCSourceAIOConfig"},{"$ref":"#/definitions/IcecastSourceAIOConfig"},{"$ref":"#/definitions/AzuracastSourceAIOConfig"},{"$ref":"#/definitions/KoitoSourceAIOConfig"},{"$ref":"#/definitions/TealSourceAIOConfig"},{"$ref":"#/definitions/RockskySourceAIOConfig"}],"title":"SourceAIOConfig"},"SpotifySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["spotify"]}},"required":["data","type"],"title":"SpotifySourceAIOConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\n\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\n\nReading:\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\n * Rate limit may ~180 req/min\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"PlexSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","type"],"title":"PlexSourceAIOConfig"},"PlexSourceData":{"type":"object","properties":{"user":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of users to scrobble tracks from\n\nIf none are provided tracks from all users will be scrobbled","examples":[["MyUser1","MyUser2"]]},"libraries":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of libraries to scrobble tracks from\n\nIf none are provided tracks from all libraries will be scrobbled","examples":[["Audio","Music"]]},"servers":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of servers to scrobble tracks from\n\nIf none are provided tracks from all servers will be scrobbled","examples":[["MyServerName"]]}},"title":"PlexSourceData"},"PlexApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","options","type"],"title":"PlexApiSourceAIOConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\n\nUseful for Plex servers using \"Required\" Secure Connections with self-signed certificates\n\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"TautulliSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["tautulli"]}},"required":["data","type"],"title":"TautulliSourceAIOConfig"},"DeezerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerSourceAIOConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerInternalAIOConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlz"]}},"required":["type"],"title":"ListenbrainzEndpointSourceAIOConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/listenbrainz/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/listenbrainz/originb\n\nIf no slug is found from an extension's incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\n\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlfm"]}},"required":["type"],"title":"LastFMEndpointSourceAIOConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/lastfm/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/lastfm/originb\n\nIf no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubsonicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["subsonic"]}},"required":["data","type"],"title":"SubsonicSourceAIOConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\n\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \"Authentication\" section here => https://www.subsonic.org/pages/api.jsp\n\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \"Token authentication not supported for LDAP users.\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","type"],"title":"JellySourceAIOConfig"},"JellyData":{"type":"object","properties":{"users":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of users to scrobble tracks from\n\nIf none are provided tracks from all users will be scrobbled","examples":[["MyUser1","MyUser2"]]},"servers":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of servers to scrobble tracks from\n\nIf none are provided tracks from all servers will be scrobbled","examples":[["MyServerName1"]]}},"title":"JellyData"},"JellyApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","options","type"],"title":"JellyApiSourceAIOConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\n\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\n\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than 'music'\n\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \"Unknown\" to be counted as Audio","default":false},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\nIf unspecified it will use the normal server HOST and PORT from the `url`\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\n\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastFmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","type"],"title":"LastFmSouceAIOConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"YTMusicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\n\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}},"type":{"type":"string","enum":["ytmusic"]}},"required":["type"],"title":"YTMusicSourceAIOConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\n\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\n\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\n\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\n\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\n\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\n\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"MPRISSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mpris"]}},"required":["data","type"],"title":"MPRISSourceAIOConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mopidy"]}},"required":["data","type"],"title":"MopidySourceAIOConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\n\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\n\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `6680`\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Don't scrobble tracks from soundcloud by adding 'soundcloud' to this list.\n\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Only scrobble tracks from soundcloud by adding 'soundcloud' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\n\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \"Soundcloud\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","type"],"title":"ListenBrainzSourceAIOConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jriver"]}},"required":["data","type"],"title":"JRiverSourceAIOConfig"},"JRiverData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `52199`\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["kodi"]}},"required":["data","type"],"title":"KodiSourceAIOConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["webscrobbler"]}},"required":["type"],"title":"WebScrobblerSourceAIOConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIn WebScrobbler's Webhook you must set an 'API URL'. All MS WebScrobbler sources must start like:\n\nhttp://localhost:9078/api/webscrobbler\n\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/webscrobbler/usera\n* slug: 'userb' => API URL: http://localhost:9078/api/webscrobbler/userb\n\nIf no slug is found from an extension's incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["chromecast"]}},"required":["data","type"],"title":"ChromecastSourceAIOConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\n\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\n\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\n\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \"media type\" in the status info returned for whatever is currently playing\n\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\n\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \"metadata\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \"media type\" reported\n\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","type"],"title":"MalojaSourceAIOConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musikcube"]}},"required":["data","type"],"title":"MusikcubeSourceAIOConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\n\nYou MUST have enabled 'metadata' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\n * musikcube -> settings -> server setup\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\n\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musiccast"]}},"required":["data","type"],"title":"MusicCastSourceAIOConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"},"type":{"type":"string","enum":["mpd"]}},"required":["data","options","type"],"title":"MPDSourceAIOConfig"},"MPDData":{"type":"object","properties":{"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\n\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\n\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"},"type":{"type":"string","enum":["vlc"]}},"required":["data","type"],"title":"VLCSourceAIOConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\n\nTo use this you must have the Web (http) interface module enabled and a password set https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\n\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\n\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"},"type":{"type":"string","enum":["icecast"]}},"required":["data","type"],"title":"IcecastSourceAIOConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\n\nIf not specified MS will use a Source's specific behavior, see Source's documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["azuracast"]}},"required":["data","type"],"title":"AzuracastSourceAIOConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\n\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\n\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\n\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\n\n* `true` => has any current listeners\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\n\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["koito"]}},"required":["data","type"],"title":"KoitoSourceAIOConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","type"],"title":"TealSourceAIOConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","type"],"title":"RockskySourceAIOConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"},"ClientAIOConfig":{"anyOf":[{"$ref":"#/definitions/MalojaClientAIOConfig"},{"$ref":"#/definitions/LastfmClientAIOConfig"},{"$ref":"#/definitions/ListenBrainzClientAIOConfig"},{"$ref":"#/definitions/KoitoClientAIOConfig"},{"$ref":"#/definitions/TealClientAIOConfig"},{"$ref":"#/definitions/RockSkyClientAIOConfig"}],"title":"ClientAIOConfig"},"MalojaClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","name","type"],"title":"MalojaClientAIOConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"LastfmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","name","type"],"title":"LastfmClientAIOConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"ListenBrainzClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","name","type"],"title":"ListenBrainzClientAIOConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["koito"]}},"required":["data","name","type"],"title":"KoitoClientAIOConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","name","type"],"title":"TealClientAIOConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","name","type"],"title":"RockSkyClientAIOConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"},"WebhookConfig":{"anyOf":[{"$ref":"#/definitions/GotifyConfig"},{"$ref":"#/definitions/NtfyConfig"},{"$ref":"#/definitions/AppriseConfig"}],"title":"WebhookConfig"},"GotifyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\n\n* gotify\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Gotify server. Same URL that would be used to reach the Gotify UI","examples":["http://192.168.0.100:8078"]},"token":{"type":"string","description":"The token created for this Application in Gotify","examples":["AQZI58fA.rfSZbm"]},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\n\n* Info -> 5\n* Warn -> 7\n* Error -> 10"}},"required":["token","type","url"],"title":"GotifyConfig"},"PrioritiesConfig":{"type":"object","properties":{"info":{"type":"number","examples":[5]},"warn":{"type":"number","examples":[7]},"error":{"type":"number","examples":[10]}},"required":["info","warn","error"],"title":"PrioritiesConfig"},"NtfyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\n\n* gotify\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Ntfy server","examples":["http://192.168.0.100:8078"]},"topic":{"type":"string","description":"The topic mutli-scrobbler should POST to"},"username":{"type":"string","description":"Required if topic is protected"},"password":{"type":"string","description":"Required if topic is protected"},"token":{"type":"string","description":"Use instead of username/password, required if topic is protected"},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\n\n* Info -> 3\n* Warn -> 4\n* Error -> 5"}},"required":["topic","type","url"],"title":"NtfyConfig"},"AppriseConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\n\n* gotify\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"host":{"type":"string","description":"The URL of the apprise-api server","examples":["http://192.168.0.100:8078"]},"urls":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Stateless Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) the Apprise config URL(s) to send"},"keys":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Persistent Store Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) the Configuration ID(s) to send to\n\nNote: If multiple keys are defined then MS will attempt to POST to each one individually"},"tags":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Optional [tag(s)](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging) to send in the notification payload"}},"required":["host","type"],"title":"AppriseConfig"},"LogOptions":{"type":"object","properties":{"level":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level for all log outputs without their own level specified.\n\nDefaults to env `LOG_LEVEL` or `info` if not specified.","default":"info"},"file":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false},{"$ref":"#/definitions/FileLogOptions"}],"description":"Specify the minimum log level to output to rotating files or file output options. If `false` no log files will be created."},"console":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level streamed to the console (or docker container)"}},"description":"Configure log levels and file options for an AppLogger.\n\n```ts\nconst infoLogger = loggerApp({\n level: 'info' // console and file will log any levels `info` and above\n});\n\nconst logger = loggerApp({\n console: 'debug', // console will log `debug` and higher\n file: 'warn' // file will log `warn` and higher\n});\n\nconst fileLogger = loggerRollingApp({\n console: 'debug', // console will log `debug` and higher\n file: {\n level: 'warn', // file will log `warn` and higher\n path: '/my/cool/path/output.log', // optionally, output to log file at this path\n frequency: 'hourly', // optionally, rotate hourly\n }\n});\n```","title":"LogOptions"},"CacheConfigOptions":{"type":"object","properties":{"metadata":{"$ref":"#/definitions/CacheMetadataConfig"},"scrobble":{"$ref":"#/definitions/CacheScrobbleConfig"},"auth":{"$ref":"#/definitions/CacheAuthConfig"}},"title":"CacheConfigOptions"},"CacheMetadataConfig":{"$ref":"#/definitions/CacheConfig%3CCacheMetadataProvider%3E","title":"CacheMetadataConfig"},"CacheConfig":{"type":"object","properties":{"provider":{"$ref":"#/definitions/CacheMetadataProvider"},"connection":{"type":"string"}},"required":["provider"],"title":"CacheConfig"},"CacheMetadataProvider":{"$ref":"#/definitions/CacheProvider","title":"CacheMetadataProvider"},"CacheProvider":{"type":["string","boolean"],"enum":["memory","valkey","file",false],"title":"CacheProvider"},"CacheScrobbleConfig":{"$ref":"#/definitions/CacheConfig%3CCacheScrobbleProvider%3E","title":"CacheScrobbleConfig"},"CacheConfig":{"type":"object","properties":{"provider":{"$ref":"#/definitions/CacheScrobbleProvider"},"connection":{"type":"string"}},"required":["provider"],"title":"CacheConfig"},"CacheScrobbleProvider":{"$ref":"#/definitions/CacheProvider","title":"CacheScrobbleProvider"},"CacheAuthConfig":{"$ref":"#/definitions/CacheConfig%3CCacheAuthProvider%3E","title":"CacheAuthConfig"},"CacheConfig":{"type":"object","properties":{"provider":{"$ref":"#/definitions/CacheAuthProvider"},"connection":{"type":"string"}},"required":["provider"],"title":"CacheConfig"},"CacheAuthProvider":{"$ref":"#/definitions/CacheProvider","title":"CacheAuthProvider"}}}
\ No newline at end of file
+{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sourceDefaults":{"$ref":"#/definitions/SourceDefaults"},"clientDefaults":{"$ref":"#/definitions/ClientDefaults"},"sources":{"type":"array","items":{"$ref":"#/definitions/SourceAIOConfig"}},"clients":{"type":"array","items":{"$ref":"#/definitions/ClientAIOConfig"}},"webhooks":{"type":"array","items":{"$ref":"#/definitions/WebhookConfig"}},"port":{"type":"number","description":"Set the port the multi-scrobbler UI will be served from","default":9078,"examples":[9078]},"baseUrl":{"type":"string","description":"Set the Base URL the application should assume the UI is served from.\n\nThis will affect how default redirect URLs are generated (spotify, lastfm, deezer) and some logging messages.\n\nIt will NOT set the actual interface/IP that the application is listening on.\n\nThis can also be set using the BASE_URL environmental variable.","default":"http://localhost","examples":["http://localhost","http://192.168.0.101","https://ms.myDomain.tld"]},"logging":{"$ref":"#/definitions/LogOptions"},"disableWeb":{"type":"boolean","description":"Disable web server from running/listening on port.\n\nThis will also make any ingress sources (Plex, Jellyfin, Tautulli, etc...) unusable"},"debugMode":{"type":"boolean","description":"Enables ALL relevant logging and debug options for all sources/clients, when none are defined.\n\nThis is a convenience shortcut for enabling all output needed to troubleshoot an issue and does not need to be on for normal operation.\n\nIt can also be enabled with the environmental variable DEBUG_MODE=true","default":false,"examples":[false]},"cache":{"$ref":"#/definitions/CacheConfigOptions"},"transformers":{"type":"array","items":{"$ref":"#/definitions/TransformerCommonConfig"}}},"definitions":{"SourceDefaults":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"SourceDefaults"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug"],"description":"Names of log levels that can be invoked on the logger\n\nFrom lowest to highest:\n\n* `debug`\n* `verbose`\n* `log`\n* `info`\n* `warn`\n* `error`\n* `fatal`\n* `silent` (will never output anything)\n\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\n\nWhen\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\n* `frequency` is defined\n\nThis determines the format of the datetime inserted into the log file name:\n\n* `unix` - unix epoch timestamp in milliseconds\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'\n* `auto`\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\n\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\n\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\n\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\n\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\n\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\n\nMay also be specified using env LOG_PATH or a function that returns a string.\n\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\n\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\n\nSet to null to disable.\n\nNOTE: This should be used with care when the Source is a \"polling\" type (has an 'interval' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \"seen\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"}],"description":"Represents the weakly-defined user config. May be an array of parts or one parts object","title":"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>"},"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},"title":"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"anyOf":[{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeMetadata"},"name":{"type":"string"},"stageHash":{"type":"string"},"score":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeUser"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string","enum":["native"]},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"name":{"type":"string"}}},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{},"artists":{},"albumArtists":{},"album":{},"duration":{},"meta":{},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]}],"title":"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"albumArtists":{"type":"string"},"album":{"type":"string"},"duration":{"type":"string"},"meta":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"Whennable":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"}},"title":"Whennable"},"FlowControlTerm":{"type":"string","enum":["continue","stop"],"title":"FlowControlTerm"},"StageTypeMetadata":{"type":"string","enum":["spotify","musicbrainz","native"],"title":"StageTypeMetadata"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"$ref":"#/definitions/ConditionalSearchAndReplaceRegExp","title":"ConditionalSearchAndReplaceTerm"},"ConditionalSearchAndReplaceRegExp":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["replace","search"],"title":"ConditionalSearchAndReplaceRegExp"},"SearchAndReplaceRegExp":{"type":"object","properties":{"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["search","replace"],"title":"SearchAndReplaceRegExp"},"ExternalMetadataTerm":{"anyOf":[{"type":"boolean"},{"not":{}},{"$ref":"#/definitions/Whennable"}],"title":"ExternalMetadataTerm"},"StageTypeUser":{"type":"string","enum":["user"],"title":"StageTypeUser"},"ClientDefaults":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"ClientDefaults"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"SourceAIOConfig":{"anyOf":[{"$ref":"#/definitions/SpotifySourceAIOConfig"},{"$ref":"#/definitions/PlexApiSourceAIOConfig"},{"$ref":"#/definitions/DeezerSourceAIOConfig"},{"$ref":"#/definitions/DeezerInternalAIOConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceAIOConfig"},{"$ref":"#/definitions/LastFMEndpointSourceAIOConfig"},{"$ref":"#/definitions/SubsonicSourceAIOConfig"},{"$ref":"#/definitions/JellyApiSourceAIOConfig"},{"$ref":"#/definitions/LastFmSouceAIOConfig"},{"$ref":"#/definitions/YTMusicSourceAIOConfig"},{"$ref":"#/definitions/MPRISSourceAIOConfig"},{"$ref":"#/definitions/MopidySourceAIOConfig"},{"$ref":"#/definitions/ListenBrainzSourceAIOConfig"},{"$ref":"#/definitions/JRiverSourceAIOConfig"},{"$ref":"#/definitions/KodiSourceAIOConfig"},{"$ref":"#/definitions/WebScrobblerSourceAIOConfig"},{"$ref":"#/definitions/ChromecastSourceAIOConfig"},{"$ref":"#/definitions/MalojaSourceAIOConfig"},{"$ref":"#/definitions/MusikcubeSourceAIOConfig"},{"$ref":"#/definitions/MusicCastSourceAIOConfig"},{"$ref":"#/definitions/MPDSourceAIOConfig"},{"$ref":"#/definitions/VLCSourceAIOConfig"},{"$ref":"#/definitions/IcecastSourceAIOConfig"},{"$ref":"#/definitions/AzuracastSourceAIOConfig"},{"$ref":"#/definitions/KoitoSourceAIOConfig"},{"$ref":"#/definitions/TealSourceAIOConfig"},{"$ref":"#/definitions/RockskySourceAIOConfig"}],"title":"SourceAIOConfig"},"SpotifySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["spotify"]}},"required":["data","type"],"title":"SpotifySourceAIOConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\n\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\n\nReading:\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\n * Rate limit may ~180 req/min\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"PlexApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"},"type":{"type":"string","enum":["plex"]}},"required":["data","type"],"title":"PlexApiSourceAIOConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\n\nUseful for Plex servers using \"Required\" Secure Connections with self-signed certificates\n\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"DeezerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerSourceAIOConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"},"type":{"type":"string","enum":["deezer"]}},"required":["data","type"],"title":"DeezerInternalAIOConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlz"]}},"required":["type"],"title":"ListenbrainzEndpointSourceAIOConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/listenbrainz/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/listenbrainz/originb\n\nIf no slug is found from an extension's incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\n\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["endpointlfm"]}},"required":["type"],"title":"LastFMEndpointSourceAIOConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/lastfm/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/lastfm/originb\n\nIf no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubsonicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["subsonic"]}},"required":["data","type"],"title":"SubsonicSourceAIOConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\n\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \"Authentication\" section here => https://www.subsonic.org/pages/api.jsp\n\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \"Token authentication not supported for LDAP users.\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellyApiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"},"type":{"type":"string","enum":["jellyfin"]}},"required":["data","options","type"],"title":"JellyApiSourceAIOConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\n\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\n\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than 'music'\n\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \"Unknown\" to be counted as Audio","default":false},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\nIf unspecified it will use the normal server HOST and PORT from the `url`\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\n\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastFmSouceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","type"],"title":"LastFmSouceAIOConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"YTMusicSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\n\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}},"type":{"type":"string","enum":["ytmusic"]}},"required":["type"],"title":"YTMusicSourceAIOConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\n\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\n\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\n\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\n\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\n\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\n\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"MPRISSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mpris"]}},"required":["data","type"],"title":"MPRISSourceAIOConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["mopidy"]}},"required":["data","type"],"title":"MopidySourceAIOConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\n\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\n\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `6680`\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Don't scrobble tracks from soundcloud by adding 'soundcloud' to this list.\n\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Only scrobble tracks from soundcloud by adding 'soundcloud' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\n\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \"Soundcloud\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","type"],"title":"ListenBrainzSourceAIOConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["jriver"]}},"required":["data","type"],"title":"JRiverSourceAIOConfig"},"JRiverData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `52199`\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["kodi"]}},"required":["data","type"],"title":"KodiSourceAIOConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["webscrobbler"]}},"required":["type"],"title":"WebScrobblerSourceAIOConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIn WebScrobbler's Webhook you must set an 'API URL'. All MS WebScrobbler sources must start like:\n\nhttp://localhost:9078/api/webscrobbler\n\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/webscrobbler/usera\n* slug: 'userb' => API URL: http://localhost:9078/api/webscrobbler/userb\n\nIf no slug is found from an extension's incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["chromecast"]}},"required":["data","type"],"title":"ChromecastSourceAIOConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\n\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\n\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\n\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \"media type\" in the status info returned for whatever is currently playing\n\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\n\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \"metadata\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \"media type\" reported\n\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","type"],"title":"MalojaSourceAIOConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musikcube"]}},"required":["data","type"],"title":"MusikcubeSourceAIOConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\n\nYou MUST have enabled 'metadata' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\n * musikcube -> settings -> server setup\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\n\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["musiccast"]}},"required":["data","type"],"title":"MusicCastSourceAIOConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"},"type":{"type":"string","enum":["mpd"]}},"required":["data","options","type"],"title":"MPDSourceAIOConfig"},"MPDData":{"type":"object","properties":{"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\n\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\n\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"},"type":{"type":"string","enum":["vlc"]}},"required":["data","type"],"title":"VLCSourceAIOConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\n\nTo use this you must have the Web (http) interface module enabled and a password set https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\n\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\n\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"},"type":{"type":"string","enum":["icecast"]}},"required":["data","type"],"title":"IcecastSourceAIOConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\n\nIf not specified MS will use a Source's specific behavior, see Source's documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"type":{"type":"string","enum":["azuracast"]}},"required":["data","type"],"title":"AzuracastSourceAIOConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\n\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\n\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\n\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\n\n* `true` => has any current listeners\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\n\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["koito"]}},"required":["data","type"],"title":"KoitoSourceAIOConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","type"],"title":"TealSourceAIOConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"},"serviceAllow":{"type":"array","items":{"type":"string"}},"serviceDeny":{"type":"array","items":{"type":"string"}}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","type"],"title":"RockskySourceAIOConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"},"ClientAIOConfig":{"anyOf":[{"$ref":"#/definitions/MalojaClientAIOConfig"},{"$ref":"#/definitions/LastfmClientAIOConfig"},{"$ref":"#/definitions/ListenBrainzClientAIOConfig"},{"$ref":"#/definitions/KoitoClientAIOConfig"},{"$ref":"#/definitions/TealClientAIOConfig"},{"$ref":"#/definitions/RockSkyClientAIOConfig"}],"title":"ClientAIOConfig"},"MalojaClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["maloja"]}},"required":["data","name","type"],"title":"MalojaClientAIOConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"LastfmClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["lastfm"]}},"required":["data","name","type"],"title":"LastfmClientAIOConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"ListenBrainzClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["listenbrainz"]}},"required":["data","name","type"],"title":"ListenBrainzClientAIOConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["koito"]}},"required":["data","name","type"],"title":"KoitoClientAIOConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["tealfm"]}},"required":["data","name","type"],"title":"TealClientAIOConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientAIOConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]},"type":{"type":"string","enum":["rocksky"]}},"required":["data","name","type"],"title":"RockSkyClientAIOConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"},"WebhookConfig":{"anyOf":[{"$ref":"#/definitions/GotifyConfig"},{"$ref":"#/definitions/NtfyConfig"},{"$ref":"#/definitions/AppriseConfig"}],"title":"WebhookConfig"},"GotifyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\n\n* gotify\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Gotify server. Same URL that would be used to reach the Gotify UI","examples":["http://192.168.0.100:8078"]},"token":{"type":"string","description":"The token created for this Application in Gotify","examples":["AQZI58fA.rfSZbm"]},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\n\n* Info -> 5\n* Warn -> 7\n* Error -> 10"}},"required":["token","type","url"],"title":"GotifyConfig"},"PrioritiesConfig":{"type":"object","properties":{"info":{"type":"number","examples":[5]},"warn":{"type":"number","examples":[7]},"error":{"type":"number","examples":[10]}},"required":["info","warn","error"],"title":"PrioritiesConfig"},"NtfyConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\n\n* gotify\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"url":{"type":"string","description":"The URL of the Ntfy server","examples":["http://192.168.0.100:8078"]},"topic":{"type":"string","description":"The topic mutli-scrobbler should POST to"},"username":{"type":"string","description":"Required if topic is protected"},"password":{"type":"string","description":"Required if topic is protected"},"token":{"type":"string","description":"Use instead of username/password, required if topic is protected"},"priorities":{"$ref":"#/definitions/PrioritiesConfig","description":"Priority of messages\n\n* Info -> 3\n* Warn -> 4\n* Error -> 5"}},"required":["topic","type","url"],"title":"NtfyConfig"},"AppriseConfig":{"type":"object","properties":{"type":{"type":"string","enum":["gotify","ntfy","apprise"],"description":"Webhook type. Valid values are:\n\n* gotify\n* ntfy","examples":["gotify"]},"name":{"type":"string","description":"A friendly name used to identify webhook config in logs"},"host":{"type":"string","description":"The URL of the apprise-api server","examples":["http://192.168.0.100:8078"]},"urls":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Stateless Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) the Apprise config URL(s) to send"},"keys":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"If using [Persistent Store Endpoints](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) the Configuration ID(s) to send to\n\nNote: If multiple keys are defined then MS will attempt to POST to each one individually"},"tags":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Optional [tag(s)](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging) to send in the notification payload"}},"required":["host","type"],"title":"AppriseConfig"},"LogOptions":{"type":"object","properties":{"level":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level for all log outputs without their own level specified.\n\nDefaults to env `LOG_LEVEL` or `info` if not specified.","default":"info"},"file":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false},{"$ref":"#/definitions/FileLogOptions"}],"description":"Specify the minimum log level to output to rotating files or file output options. If `false` no log files will be created."},"console":{"$ref":"#/definitions/LogLevel","description":"Specify the minimum log level streamed to the console (or docker container)"}},"description":"Configure log levels and file options for an AppLogger.\n\n```ts\nconst infoLogger = loggerApp({\n level: 'info' // console and file will log any levels `info` and above\n});\n\nconst logger = loggerApp({\n console: 'debug', // console will log `debug` and higher\n file: 'warn' // file will log `warn` and higher\n});\n\nconst fileLogger = loggerRollingApp({\n console: 'debug', // console will log `debug` and higher\n file: {\n level: 'warn', // file will log `warn` and higher\n path: '/my/cool/path/output.log', // optionally, output to log file at this path\n frequency: 'hourly', // optionally, rotate hourly\n }\n});\n```","title":"LogOptions"},"CacheConfigOptions":{"type":"object","properties":{"metadata":{"$ref":"#/definitions/CacheMetadataConfig"},"scrobble":{"$ref":"#/definitions/CacheScrobbleConfig"},"auth":{"$ref":"#/definitions/CacheAuthConfig"},"regex":{"type":"number","description":"Number of regex functions to cache (LRU)","default":200}},"title":"CacheConfigOptions"},"CacheMetadataConfig":{"$ref":"#/definitions/CacheConfig%3CCacheMetadataProvider%3E","title":"CacheMetadataConfig"},"CacheConfig":{"type":"object","properties":{"provider":{"$ref":"#/definitions/CacheMetadataProvider"},"connection":{"type":"string"}},"required":["provider"],"title":"CacheConfig"},"CacheMetadataProvider":{"$ref":"#/definitions/CacheProvider","title":"CacheMetadataProvider"},"CacheProvider":{"type":["string","boolean"],"enum":["memory","valkey","file",false],"title":"CacheProvider"},"CacheScrobbleConfig":{"$ref":"#/definitions/CacheConfig%3CCacheScrobbleProvider%3E","title":"CacheScrobbleConfig"},"CacheConfig":{"type":"object","properties":{"provider":{"$ref":"#/definitions/CacheScrobbleProvider"},"connection":{"type":"string"}},"required":["provider"],"title":"CacheConfig"},"CacheScrobbleProvider":{"$ref":"#/definitions/CacheProvider","title":"CacheScrobbleProvider"},"CacheAuthConfig":{"$ref":"#/definitions/CacheConfig%3CCacheAuthProvider%3E","title":"CacheAuthConfig"},"CacheConfig":{"type":"object","properties":{"provider":{"$ref":"#/definitions/CacheAuthProvider"},"connection":{"type":"string"}},"required":["provider"],"title":"CacheConfig"},"CacheAuthProvider":{"$ref":"#/definitions/CacheProvider","title":"CacheAuthProvider"},"TransformerCommonConfig":{"type":"object","properties":{"defaults":{"type":"object"},"data":{"type":"object"},"type":{"type":"string"},"name":{"type":"string"},"options":{"$ref":"#/definitions/TransformOptions"}},"required":["type"],"title":"TransformerCommonConfig"},"TransformOptions":{"type":"object","properties":{"failOnFetch":{"type":"boolean"},"throwOnFailure":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string","enum":["artists","title","albumArtists","album","duration","meta"]}}]},"ttl":{"type":"string"}},"title":"TransformOptions"}}}
\ No newline at end of file
diff --git a/src/backend/common/schema/client.json b/src/backend/common/schema/client.json
index e405d03c..00b274db 100644
--- a/src/backend/common/schema/client.json
+++ b/src/backend/common/schema/client.json
@@ -1 +1 @@
-{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"$ref":"#/definitions/MalojaClientConfig"},{"$ref":"#/definitions/LastfmClientConfig"},{"$ref":"#/definitions/ListenBrainzClientConfig"},{"$ref":"#/definitions/KoitoClientConfig"},{"$ref":"#/definitions/TealClientConfig"},{"$ref":"#/definitions/RockSkyClientConfig"}],"definitions":{"MalojaClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"MalojaClientConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3CSearchAndReplaceTerm%3E"},{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"}],"title":"PlayTransformPartsConfig"},"PlayTransformPartsArray":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"},"title":"PlayTransformPartsArray"},"PlayTransformParts":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"artists":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"album":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}}},"title":"PlayTransformParts"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"album":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{},"replace":{}},"required":["search","replace"],"title":"ConditionalSearchAndReplaceTerm"},"LastfmClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"LastfmClientConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"ListenBrainzClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"ListenBrainzClientConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"KoitoClientConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"TealClientConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"RockSkyClientConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"}}}
\ No newline at end of file
+{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"$ref":"#/definitions/MalojaClientConfig"},{"$ref":"#/definitions/LastfmClientConfig"},{"$ref":"#/definitions/ListenBrainzClientConfig"},{"$ref":"#/definitions/KoitoClientConfig"},{"$ref":"#/definitions/TealClientConfig"},{"$ref":"#/definitions/RockSkyClientConfig"}],"definitions":{"MalojaClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/MalojaClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Maloja as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"MalojaClientConfig"},"MalojaClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaClientData"},"CommonClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonClientOptions"},"MatchLoggingOptions":{"type":"object","properties":{"onNoMatch":{"type":"boolean","description":"Log to DEBUG when a new track does NOT match an existing scrobble","default":false,"examples":[false]},"onMatch":{"type":"boolean","description":"Log to DEBUG when a new track DOES match an existing scrobble","default":false,"examples":[false]},"confidenceBreakdown":{"type":"boolean","description":"Include confidence breakdowns in track match logging, if applicable","default":false,"examples":[false]}},"description":"Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.","title":"MatchLoggingOptions"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"}],"description":"Represents the weakly-defined user config. May be an array of parts or one parts object","title":"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>"},"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},"title":"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"anyOf":[{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeMetadata"},"name":{"type":"string"},"stageHash":{"type":"string"},"score":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeUser"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string","enum":["native"]},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"name":{"type":"string"}}},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{},"artists":{},"albumArtists":{},"album":{},"duration":{},"meta":{},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]}],"title":"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"albumArtists":{"type":"string"},"album":{"type":"string"},"duration":{"type":"string"},"meta":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"Whennable":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"}},"title":"Whennable"},"FlowControlTerm":{"type":"string","enum":["continue","stop"],"title":"FlowControlTerm"},"StageTypeMetadata":{"type":"string","enum":["spotify","musicbrainz","native"],"title":"StageTypeMetadata"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"$ref":"#/definitions/ConditionalSearchAndReplaceRegExp","title":"ConditionalSearchAndReplaceTerm"},"ConditionalSearchAndReplaceRegExp":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["replace","search"],"title":"ConditionalSearchAndReplaceRegExp"},"SearchAndReplaceRegExp":{"type":"object","properties":{"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["search","replace"],"title":"SearchAndReplaceRegExp"},"ExternalMetadataTerm":{"anyOf":[{"type":"boolean"},{"not":{}},{"$ref":"#/definitions/Whennable"}],"title":"ExternalMetadataTerm"},"StageTypeUser":{"type":"string","enum":["user"],"title":"StageTypeUser"},"LastfmClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/LastfmData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/LastfmClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using LastFM as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"LastfmClientConfig"},"LastfmData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]}},"required":["apiKey","secret"],"title":"LastfmData"},"LastfmClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"LastfmClientOptions"},"ListenBrainzClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/ListenBrainzClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Listenbrainz as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"ListenBrainzClientConfig"},"ListenBrainzClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzClientData"},"KoitoClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/KoitoClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/CommonClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Koito as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"KoitoClientConfig"},"KoitoClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoClientData"},"TealClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/TealClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/TealClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using Tealfm as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"TealClientConfig"},"TealClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealClientData"},"TealClientOptions":{"type":"object","properties":{"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealClientOptions"},"RockSkyClientConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this client. Used with sources to restrict where scrobbles are sent.","examples":["MyConfig"]},"data":{"$ref":"#/definitions/RockSkyClientData","description":"Specific data required to configure this client"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"options":{"$ref":"#/definitions/RockSkyClientOptions"},"configureAs":{"type":"string","enum":["client","source"],"description":"Should always be `client` when using RockSky as a client","default":"client","examples":["client"]}},"required":["data","name"],"title":"RockSkyClientConfig"},"RockSkyClientData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockSkyClientData"},"RockSkyClientOptions":{"type":"object","properties":{"nowPlaying":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Configure if this Client should report Now Playing from Sources that can scrobble to it\n\n* `true` (default) => Report Now Playing from any eligible Source. \n * If multiple Sources are Playing then reported Play is based on alphabetical order of Source names\n* `false` => Do not report Now Playing\n* `string` list => list of Source `names` that should be allowed to report Now Playing. Order of list determine priority of Play to Report.","default":true},"refreshEnabled":{"type":"boolean","description":"Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history","default":true,"examples":[true]},"refreshStaleAfter":{"type":"number","description":"Refresh scrobbled plays from upstream service if last refresh was at least X seconds ago\n\n**In most case this setting does NOT need to be changed.** The default value is sufficient for the majority of use-cases. Increasing this setting may increase upstream service load and slow down scrobbles.\n\nThis setting should only be changed in specific scenarios where MS is handling multiple \"relaying\" client-services (IE lfm -> lz -> lfm) and there is the potential for a client to be out of sync after more than a few seconds.","examples":[60],"default":60},"refreshMinInterval":{"type":"number","description":"Minimum time (milliseconds) required to pass before upstream scrobbles can be refreshed.\n\n**In most case this setting does NOT need to be changed.** This will always be equal to or smaller than `refreshStaleAfter`.","default":5000,"examples":[5000]},"refreshInitialCount":{"type":"number","description":"The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported by the client in 1 API call."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"checkExistingScrobbles":{"type":"boolean","description":"Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.","default":true,"examples":[true]},"verbose":{"type":"object","properties":{"match":{"$ref":"#/definitions/MatchLoggingOptions"}},"description":"Options used for increasing verbosity of logging in MS (used for debugging)"},"deadLetterRetries":{"type":"number","description":"Number of times MS should automatically retry scrobbles in dead letter queue","default":1,"examples":[1]},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockSkyClientOptions"}}}
\ No newline at end of file
diff --git a/src/backend/common/schema/source.json b/src/backend/common/schema/source.json
index 26705a06..98dd7175 100644
--- a/src/backend/common/schema/source.json
+++ b/src/backend/common/schema/source.json
@@ -1 +1 @@
-{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"$ref":"#/definitions/SpotifySourceConfig"},{"$ref":"#/definitions/PlexSourceConfig"},{"$ref":"#/definitions/PlexApiSourceConfig"},{"$ref":"#/definitions/TautulliSourceConfig"},{"$ref":"#/definitions/DeezerSourceConfig"},{"$ref":"#/definitions/DeezerInternalSourceConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceConfig"},{"$ref":"#/definitions/LastFMEndpointSourceConfig"},{"$ref":"#/definitions/SubSonicSourceConfig"},{"$ref":"#/definitions/JellySourceConfig"},{"$ref":"#/definitions/JellyApiSourceConfig"},{"$ref":"#/definitions/LastfmSourceConfig"},{"$ref":"#/definitions/YTMusicSourceConfig"},{"$ref":"#/definitions/MPRISSourceConfig"},{"$ref":"#/definitions/MopidySourceConfig"},{"$ref":"#/definitions/ListenBrainzSourceConfig"},{"$ref":"#/definitions/JRiverSourceConfig"},{"$ref":"#/definitions/KodiSourceConfig"},{"$ref":"#/definitions/WebScrobblerSourceConfig"},{"$ref":"#/definitions/ChromecastSourceConfig"},{"$ref":"#/definitions/MalojaSourceConfig"},{"$ref":"#/definitions/MusikcubeSourceConfig"},{"$ref":"#/definitions/MusicCastSourceConfig"},{"$ref":"#/definitions/MPDSourceConfig"},{"$ref":"#/definitions/VLCSourceConfig"},{"$ref":"#/definitions/IcecastSourceConfig"},{"$ref":"#/definitions/AzuracastSourceConfig"},{"$ref":"#/definitions/KoitoSourceConfig"},{"$ref":"#/definitions/TealSourceConfig"},{"$ref":"#/definitions/RockskySourceConfig"}],"definitions":{"SpotifySourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"SpotifySourceConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\n\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\n\nReading:\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\n * Rate limit may ~180 req/min\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug"],"description":"Names of log levels that can be invoked on the logger\n\nFrom lowest to highest:\n\n* `debug`\n* `verbose`\n* `log`\n* `info`\n* `warn`\n* `error`\n* `fatal`\n* `silent` (will never output anything)\n\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\n\nWhen\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\n* `frequency` is defined\n\nThis determines the format of the datetime inserted into the log file name:\n\n* `unix` - unix epoch timestamp in milliseconds\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'\n* `auto`\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\n\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\n\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\n\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\n\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\n\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\n\nMay also be specified using env LOG_PATH or a function that returns a string.\n\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\n\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\n\nSet to null to disable.\n\nNOTE: This should be used with care when the Source is a \"polling\" type (has an 'interval' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \"seen\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3CSearchAndReplaceTerm%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3CSearchAndReplaceTerm%3E"},{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"}],"title":"PlayTransformPartsConfig"},"PlayTransformPartsArray":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3CSearchAndReplaceTerm%3E"},"title":"PlayTransformPartsArray"},"PlayTransformParts":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"artists":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},"album":{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}}},"title":"PlayTransformParts"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"album":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{},"replace":{}},"required":["search","replace"],"title":"ConditionalSearchAndReplaceTerm"},"PlexSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"PlexSourceConfig"},"PlexSourceData":{"type":"object","properties":{"user":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of users to scrobble tracks from\n\nIf none are provided tracks from all users will be scrobbled","examples":[["MyUser1","MyUser2"]]},"libraries":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of libraries to scrobble tracks from\n\nIf none are provided tracks from all libraries will be scrobbled","examples":[["Audio","Music"]]},"servers":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of servers to scrobble tracks from\n\nIf none are provided tracks from all servers will be scrobbled","examples":[["MyServerName"]]}},"title":"PlexSourceData"},"PlexApiSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"}},"required":["data","options"],"title":"PlexApiSourceConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\n\nUseful for Plex servers using \"Required\" Secure Connections with self-signed certificates\n\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"TautulliSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"TautulliSourceConfig"},"DeezerSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"DeezerSourceConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"}},"required":["data"],"title":"DeezerInternalSourceConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"title":"ListenbrainzEndpointSourceConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/listenbrainz/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/listenbrainz/originb\n\nIf no slug is found from an extension's incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\n\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"title":"LastFMEndpointSourceConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/lastfm/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/lastfm/originb\n\nIf no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubSonicSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"SubSonicSourceConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\n\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \"Authentication\" section here => https://www.subsonic.org/pages/api.jsp\n\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \"Token authentication not supported for LDAP users.\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellySourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"JellySourceConfig"},"JellyData":{"type":"object","properties":{"users":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of users to scrobble tracks from\n\nIf none are provided tracks from all users will be scrobbled","examples":[["MyUser1","MyUser2"]]},"servers":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"optional list of servers to scrobble tracks from\n\nIf none are provided tracks from all servers will be scrobbled","examples":[["MyServerName1"]]}},"title":"JellyData"},"JellyApiSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"}},"required":["data","options"],"title":"JellyApiSourceConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\n\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\n\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than 'music'\n\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \"Unknown\" to be counted as Audio","default":false},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\nIf unspecified it will use the normal server HOST and PORT from the `url`\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\n\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastfmSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"LastfmSourceConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"YTMusicSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\n\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}}},"title":"YTMusicSourceConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\n\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\n\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\n\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\n\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\n\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\n\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"MPRISSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MPRISSourceConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MopidySourceConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\n\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\n\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `6680`\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Don't scrobble tracks from soundcloud by adding 'soundcloud' to this list.\n\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Only scrobble tracks from soundcloud by adding 'soundcloud' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\n\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \"Soundcloud\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"ListenBrainzSourceConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"JRiverSourceConfig"},"JRiverData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `52199`\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"KodiSourceConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"title":"WebScrobblerSourceConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIn WebScrobbler's Webhook you must set an 'API URL'. All MS WebScrobbler sources must start like:\n\nhttp://localhost:9078/api/webscrobbler\n\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/webscrobbler/usera\n* slug: 'userb' => API URL: http://localhost:9078/api/webscrobbler/userb\n\nIf no slug is found from an extension's incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"ChromecastSourceConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\n\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\n\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\n\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \"media type\" in the status info returned for whatever is currently playing\n\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\n\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \"metadata\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \"media type\" reported\n\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"MalojaSourceConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MusikcubeSourceConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\n\nYou MUST have enabled 'metadata' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\n * musikcube -> settings -> server setup\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\n\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MusicCastSourceConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"}},"required":["data","options"],"title":"MPDSourceConfig"},"MPDData":{"type":"object","properties":{"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\n\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\n\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"}},"required":["data"],"title":"VLCSourceConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\n\nTo use this you must have the Web (http) interface module enabled and a password set https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\n\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\n\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"}},"required":["data"],"title":"IcecastSourceConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\n\nIf not specified MS will use a Source's specific behavior, see Source's documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"AzuracastSourceConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\n\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\n\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\n\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\n\n* `true` => has any current listeners\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\n\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"KoitoSourceConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]}},"required":["data"],"title":"TealSourceConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"RockskySourceConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"}}}
\ No newline at end of file
+{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"$ref":"#/definitions/SpotifySourceConfig"},{"$ref":"#/definitions/PlexApiSourceConfig"},{"$ref":"#/definitions/DeezerSourceConfig"},{"$ref":"#/definitions/DeezerInternalSourceConfig"},{"$ref":"#/definitions/ListenbrainzEndpointSourceConfig"},{"$ref":"#/definitions/LastFMEndpointSourceConfig"},{"$ref":"#/definitions/SubSonicSourceConfig"},{"$ref":"#/definitions/JellyApiSourceConfig"},{"$ref":"#/definitions/LastfmSourceConfig"},{"$ref":"#/definitions/YTMusicSourceConfig"},{"$ref":"#/definitions/MPRISSourceConfig"},{"$ref":"#/definitions/MopidySourceConfig"},{"$ref":"#/definitions/ListenBrainzSourceConfig"},{"$ref":"#/definitions/JRiverSourceConfig"},{"$ref":"#/definitions/KodiSourceConfig"},{"$ref":"#/definitions/WebScrobblerSourceConfig"},{"$ref":"#/definitions/ChromecastSourceConfig"},{"$ref":"#/definitions/MalojaSourceConfig"},{"$ref":"#/definitions/MusikcubeSourceConfig"},{"$ref":"#/definitions/MusicCastSourceConfig"},{"$ref":"#/definitions/MPDSourceConfig"},{"$ref":"#/definitions/VLCSourceConfig"},{"$ref":"#/definitions/IcecastSourceConfig"},{"$ref":"#/definitions/AzuracastSourceConfig"},{"$ref":"#/definitions/KoitoSourceConfig"},{"$ref":"#/definitions/TealSourceConfig"},{"$ref":"#/definitions/RockskySourceConfig"}],"definitions":{"SpotifySourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SpotifySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"SpotifySourceConfig"},"SpotifySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)\n\nIt is unlikely you should need to change this unless you scrobble many very short tracks often\n\nReading:\n* https://developer.spotify.com/documentation/web-api/guides/rate-limits/\n* https://medium.com/mendix/limiting-your-amount-of-calls-in-mendix-most-of-the-time-rest-835dde55b10e\n * Rate limit may ~180 req/min\n* https://community.spotify.com/t5/Spotify-for-Developers/Web-API-ratelimit/m-p/5503150/highlight/true#M7930\n * Informally indicated as 20 req/sec? Probably for burstiness","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"spotify client id","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"clientSecret":{"type":"string","description":"spotify client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"spotify redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/callback","examples":["http://localhost:9078/callback"]}},"required":["clientId","clientSecret"],"title":"SpotifySourceData"},"CommonSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"CommonSourceOptions"},"LogLevel":{"type":"string","enum":["silent","fatal","error","warn","info","log","verbose","debug"],"description":"Names of log levels that can be invoked on the logger\n\nFrom lowest to highest:\n\n* `debug`\n* `verbose`\n* `log`\n* `info`\n* `warn`\n* `error`\n* `fatal`\n* `silent` (will never output anything)\n\nWhen used in `LogOptions` specifies the **minimum** level the output should log at.","title":"LogLevel"},"FileLogOptions":{"type":"object","properties":{"timestamp":{"type":"string","enum":["unix","iso","auto"],"description":"For rolling log files\n\nWhen\n* value passed to rolling destination is a string (`path` from LogOptions is a string) and\n* `frequency` is defined\n\nThis determines the format of the datetime inserted into the log file name:\n\n* `unix` - unix epoch timestamp in milliseconds\n* `iso` - Full [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime IE '2024-03-07T20:11:34Z'\n* `auto`\n * When frequency is `daily` only inserts date IE YYYY-MM-DD\n * Otherwise inserts full ISO8601 datetime","default":"auto"},"size":{"type":["number","string"],"description":"The maximum size of a given rolling log file.\n\nCan be combined with frequency. Use k, m and g to express values in KB, MB or GB.\n\nNumerical values will be considered as MB.","default":"10MB"},"frequency":{"anyOf":[{"type":"string","enum":["daily"]},{"type":"string","enum":["hourly"]},{"type":"number"}],"description":"The amount of time a given rolling log file is used. Can be combined with size.\n\nUse `daily` or `hourly` to rotate file every day (or every hour). Existing file within the current day (or hour) will be re-used.\n\nNumerical values will be considered as a number of milliseconds. Using a numerical value will always create a new file upon startup.","default":"daily"},"path":{"anyOf":[{"type":"string"},{"$comment":"() => string"}],"description":"The path and filename to use for log files.\n\nIf using rolling files the filename will be appended with `.N` (a number) BEFORE the extension based on rolling status.\n\nMay also be specified using env LOG_PATH or a function that returns a string.\n\nIf path is relative the absolute path will be derived from `logBaseDir` (in `LoggerAppExtras`) which defaults to CWD","default":"./logs/app.log"},"level":{"anyOf":[{"$ref":"#/definitions/LogLevel"},{"type":"boolean","const":false}],"description":"Specify the minimum log level to output to rotating files. If `false` no log files will be created."}},"title":"FileLogOptions"},"ScrobbleThresholds":{"type":"object","properties":{"duration":{"type":["number","null"],"description":"The number of seconds a track has been listened to before it should be considered scrobbled.\n\nSet to null to disable.","default":240,"examples":[240]},"percent":{"type":["number","null"],"description":"The percentage (as an integer) of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.\n\nSet to null to disable.\n\nNOTE: This should be used with care when the Source is a \"polling\" type (has an 'interval' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not \"seen\" the track for long enough from first discovery, even if you have been playing the track for longer.","default":50,"examples":[50]}},"title":"ScrobbleThresholds"},"PlayTransformOptions":{"type":"object","properties":{"log":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["all"]}]},"preCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"compare":{"type":"object","properties":{"candidate":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"},"existing":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}}},"postCompare":{"$ref":"#/definitions/PlayTransformPartsConfig%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%3E"}},"title":"PlayTransformOptions"},"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>":{"anyOf":[{"$ref":"#/definitions/PlayTransformPartsArray%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"}],"description":"Represents the weakly-defined user config. May be an array of parts or one parts object","title":"PlayTransformPartsConfig<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm)>"},"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"type":"array","items":{"$ref":"#/definitions/PlayTransformParts%3C(def-alias-617285649-262-339-617285649-0-4937%5B%5D%7CExternalMetadataTerm)%2CMaybeStageTyped%3E"},"title":"PlayTransformPartsArray<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>":{"anyOf":[{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeMetadata"},"name":{"type":"string"},"stageHash":{"type":"string"},"score":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"$ref":"#/definitions/StageTypeUser"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"artists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"albumArtists":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"album":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"duration":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"meta":{"anyOf":[{"type":"boolean"},{"$ref":"#/definitions/Whennable"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string","enum":["native"]},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"artists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"albumArtists":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"album":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"duration":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"meta":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/SearchAndReplaceTerm"}},{"$ref":"#/definitions/ExternalMetadataTerm"}]},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"name":{"type":"string"}}},{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"title":{},"artists":{},"albumArtists":{},"album":{},"duration":{},"meta":{},"onSuccess":{"$ref":"#/definitions/FlowControlTerm"},"onFailure":{"$ref":"#/definitions/FlowControlTerm"},"onSkip":{"$ref":"#/definitions/FlowControlTerm"},"failureReturnPartial":{"type":"boolean"},"type":{"type":"string"},"name":{"type":"string"},"stageHash":{"type":"string"}},"required":["type"]}],"title":"PlayTransformParts<(def-alias-617285649-262-339-617285649-0-4937[]|ExternalMetadataTerm),MaybeStageTyped>"},"WhenConditionsConfig":{"$ref":"#/definitions/WhenConditions%3Cstring%3E","title":"WhenConditionsConfig"},"WhenConditions":{"type":"array","items":{"$ref":"#/definitions/WhenParts%3Cstring%3E"},"title":"WhenConditions"},"WhenParts":{"$ref":"#/definitions/PlayTransformPartsAtomic%3Cstring%3E","title":"WhenParts"},"PlayTransformPartsAtomic":{"type":"object","properties":{"title":{"type":"string"},"artists":{"type":"string"},"albumArtists":{"type":"string"},"album":{"type":"string"},"duration":{"type":"string"},"meta":{"type":"string"}},"title":"PlayTransformPartsAtomic"},"Whennable":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"}},"title":"Whennable"},"FlowControlTerm":{"type":"string","enum":["continue","stop"],"title":"FlowControlTerm"},"StageTypeMetadata":{"type":"string","enum":["spotify","musicbrainz","native"],"title":"StageTypeMetadata"},"SearchAndReplaceTerm":{"anyOf":[{"type":"string"},{"$ref":"#/definitions/ConditionalSearchAndReplaceTerm"}],"title":"SearchAndReplaceTerm"},"ConditionalSearchAndReplaceTerm":{"$ref":"#/definitions/ConditionalSearchAndReplaceRegExp","title":"ConditionalSearchAndReplaceTerm"},"ConditionalSearchAndReplaceRegExp":{"type":"object","properties":{"when":{"$ref":"#/definitions/WhenConditionsConfig"},"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["replace","search"],"title":"ConditionalSearchAndReplaceRegExp"},"SearchAndReplaceRegExp":{"type":"object","properties":{"search":{"anyOf":[{"type":"string"},{"type":"string","format":"regex"}],"description":"The search value to test for\n\nCan be a normal string (converted to a case-sensitive literal) or a valid regular expression as a string, or an actual RegExp object","examples":[["find this string","/some string*/ig"]]},"replace":{"type":"string","description":"The replacement string/value to use when search is found\n\nThis can be a literal string like `'replace with this`, an empty string to remove the search value (`''`), or a special regex value\n\nSee replacement here for more information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace"},"test":{"$comment":"(obj: SearchAndReplaceRegExp) => boolean","type":"object","properties":{"namedArgs":{"type":"object","properties":{"obj":{"$ref":"#/definitions/SearchAndReplaceRegExp"}},"required":["obj"],"additionalProperties":false}},"description":"An optional function to execute before running this search-and-replace that decides if it should run at all"}},"required":["search","replace"],"title":"SearchAndReplaceRegExp"},"ExternalMetadataTerm":{"anyOf":[{"type":"boolean"},{"not":{}},{"$ref":"#/definitions/Whennable"}],"title":"ExternalMetadataTerm"},"StageTypeUser":{"type":"string","enum":["user"],"title":"StageTypeUser"},"PlexApiSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/PlexApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/PlexApiOptions"}},"required":["data"],"title":"PlexApiSourceConfig"},"PlexApiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"token":{"type":"string"},"url":{"type":"string","description":"http(s)://HOST:PORT of the Plex server to connect to"},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"}},"required":["url"],"title":"PlexApiData"},"PlexApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"ignoreInvalidCert":{"type":"boolean","description":"Ignore invalid cert errors when connecting to Plex\n\nUseful for Plex servers using \"Required\" Secure Connections with self-signed certificates\n\nDo not enable unless you know you need this.","default":false}},"title":"PlexApiOptions"},"DeezerSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"DeezerSourceConfig"},"DeezerData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"clientId":{"type":"string","description":"deezer client id","examples":["a89cba1569901a0671d5a9875fed4be1"]},"clientSecret":{"type":"string","description":"deezer client secret","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"redirectUri":{"type":"string","description":"deezer redirect URI -- required only if not the default shown here. URI must end in \"callback\"","default":"http://localhost:9078/deezer/callback","examples":["http://localhost:9078/deezer/callback"]}},"required":["clientId","clientSecret"],"title":"DeezerData"},"DeezerInternalSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/DeezerInternalData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/DeezerInternalSourceOptions"}},"required":["data"],"title":"DeezerInternalSourceConfig"},"DeezerInternalData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"arl":{"type":"string","description":"ARL retrieved from Deezer response header"},"userAgent":{"type":"string","description":"User agent","default":"Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"}},"required":["arl"],"title":"DeezerInternalData"},"DeezerInternalSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"fuzzyDiscoveryIgnore":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["aggressive"]}]}},"title":"DeezerInternalSourceOptions"},"ListenbrainzEndpointSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenbrainzEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"title":"ListenbrainzEndpointSourceConfig"},"ListenbrainzEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Listenbrainz endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/listenbrainz/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/listenbrainz/originb\n\nIf no slug is found from an extension's incoming webhook event the first Listenbrainz source without a slug will be used"},"token":{"type":["string","null"],"description":"If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source\n\nSee: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests"}},"title":"ListenbrainzEndpointData"},"LastFMEndpointSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFMEndpointData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"title":"LastFMEndpointSourceConfig"},"LastFMEndpointData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIf you are using multiple Last.fm endpoint sources (scrobbles for many users) you can use a slug to match Sources with individual users/origins\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/lastfm/usera\n* slug: 'originb' => API URL: http://localhost:9078/api/lastfm/originb\n\nIf no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used"}},"title":"LastFMEndpointData"},"SubSonicSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/SubsonicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"SubSonicSourceConfig"},"SubsonicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the subsonic media server to query","examples":["http://airsonic.local"]},"user":{"type":"string","description":"Username to login to the server with","examples":[["MyUser"]]},"password":{"type":"string","description":"Password for the user to login to the server with","examples":["MyPassword"]},"ignoreTlsErrors":{"type":"boolean","description":"If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection\n\nWARNING: This should be used with caution as your traffic may not be encrypted.","default":false},"legacyAuthentication":{"type":"boolean","description":"Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See \"Authentication\" section here => https://www.subsonic.org/pages/api.jsp\n\nIf this option is not specified it will be turned on if the subsonic server responds with error code 41 \"Token authentication not supported for LDAP users.\" -- See Error Handling section => https://www.subsonic.org/pages/api.jsp","default":false},"usersAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf undefined or an empty string/list MS will scrobble activity from all users"}},"required":["url","user","password"],"title":"SubsonicData"},"JellyApiSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JellyApiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/JellyApiOptions"}},"required":["data","options"],"title":"JellyApiSourceConfig"},"JellyApiData":{"type":"object","properties":{"url":{"type":"string","description":"HOST:PORT of the Jellyfin server to connect to"},"user":{"type":"string","description":"The username of the user to authenticate for or track scrobbles for"},"password":{"type":"string","description":"Password of the username to authenticate for\n\nRequired if `apiKey` is not provided."},"apiKey":{"type":"string","description":"API Key to authenticate with.\n\nRequired if `password` is not provided."},"usersAllow":{"anyOf":[{"type":"string"},{"type":"boolean","enum":[true]},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble for specific users (case-insensitive)\n\nIf `true` MS will scrobble activity from all users"},"usersBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble for these users (case-insensitive)"},"devicesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if device or application name contains strings from this list (case-insensitive)"},"devicesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if device or application name contains strings from this list (case-insensitive)"},"librariesAllow":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only scrobble if library name contains string from this list (case-insensitive)"},"librariesBlock":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Do not scrobble if library name contains strings from this list (case-insensitive)"},"additionalAllowedLibraryTypes":{"type":"array","items":{},"description":"Allow MS to scrobble audio media in libraries classified other than 'music'\n\n`librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock`"},"allowUnknown":{"type":"boolean","description":"Force media with a type of \"Unknown\" to be counted as Audio","default":false},"frontendUrlOverride":{"type":"string","description":"HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)\nIf unspecified it will use the normal server HOST and PORT from the `url`\nNecessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly\n\nENV: JELLYFIN_FRONTEND_URL_OVERRIDE"}},"required":["url","user"],"title":"JellyApiData"},"JellyApiOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"JellyApiOptions"},"LastfmSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/LastFmSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"LastfmSourceConfig"},"LastFmSourceData":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"apiKey":{"type":"string","description":"API Key generated from Last.fm account","examples":["787c921a2a2ab42320831aba0c8f2fc2"]},"secret":{"type":"string","description":"Secret generated from Last.fm account","examples":["ec42e09d5ae0ee0f0816ca151008412a"]},"session":{"type":"string","description":"Optional session id returned from a completed auth flow"},"redirectUri":{"type":"string","description":"Optional URI to use for callback. Specify this if callback should be different than the default. MUST have \"lastfm/callback\" in the URL somewhere.","default":"http://localhost:9078/lastfm/callback","examples":["http://localhost:9078/lastfm/callback"]},"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"}},"required":["apiKey","secret"],"title":"LastFmSourceData"},"YTMusicSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/YTMusicData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"type":"object","properties":{"logAuth":{"type":"boolean","description":"When true MS will log to DEBUG all of the credentials data it receives from YTM"},"logDiff":{"type":"boolean","description":"Always log history diff\n\nBy default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found)\nSet this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN`","default":false},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}}}},"title":"YTMusicSourceConfig"},"YTMusicData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"cookie":{"type":"string","description":"The cookie retrieved from the Request Headers of music.youtube.com after logging in.\n\nSee https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value.","examples":["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."]},"clientId":{"type":"string","description":"Google Cloud Console project OAuth Client ID\n\nGenerated from a custom OAuth Client, see docs"},"clientSecret":{"type":"string","description":"Google Cloud Console project OAuth Client Secret\n\nGenerated from a custom OAuth Client, see docs"},"redirectUri":{"type":"string","description":"Google Cloud Console project OAuth Client Authorized redirect URI\n\nGenerated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL.\nOnly specify this if the default does not work for you."},"innertubeOptions":{"$ref":"#/definitions/InnertubeOptions","description":"Additional options for authorization and tailoring YTM client"}},"title":"YTMusicData"},"InnertubeOptions":{"type":"object","properties":{"po_token":{"type":"string","description":"Proof of Origin token\n\nMay be required if YTM starts returning 403"},"visitor_data":{"type":"string","description":"Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie\n\nMay be required if YTM starts returning 403"},"account_index":{"type":"number","description":"If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor","examples":[0,1]},"location":{"type":"string"},"lang":{"type":"string"},"generate_session_locally":{"type":"boolean"},"device_category":{"type":"string"},"client_type":{"type":"string"},"timezone":{"type":"string"}},"title":"InnertubeOptions"},"MPRISSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPRISData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MPRISSourceConfig"},"MPRISData":{"type":"object","properties":{"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any players that START WITH these values, case-insensitive","examples":[["spotify","vlc"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","vlc"]]}},"title":"MPRISData"},"MopidySourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MopidyData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MopidySourceConfig"},"MopidyData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Mopidy HTTP server to connect to\n\nYou MUST have Mopidy-HTTP extension enabled: https://mopidy.com/ext/http\n\nmulti-scrobbler connects to the WebSocket endpoint that ultimately looks like this => `ws://localhost:6680/mopidy/ws/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `6680`\n* Path => `/mopidy/ws/`","examples":["ws://localhost:6680/mopidy/ws/"],"default":"ws://localhost:6680/mopidy/ws/"},"uriBlacklist":{"type":"array","items":{"type":"string"},"description":"Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Don't scrobble tracks from soundcloud by adding 'soundcloud' to this list.\n\nList is ignored if uriWhitelist is used."},"uriWhitelist":{"type":"array","items":{"type":"string"},"description":"Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive\n\nEX: Only scrobble tracks from soundcloud by adding 'soundcloud' to this list."},"albumBlacklist":{"type":"array","items":{"type":"string"},"description":"Remove album data that matches any case-insensitive string from this list when scrobbling,\n\nFor certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use \"Soundcloud\" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake name to this list. Multi-scrobbler will still scrobble the track, just without the bad data.","examples":[["Soundcloud","Mixcloud"]],"default":["Soundcloud"]}},"title":"MopidyData"},"ListenBrainzSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ListenBrainzSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"ListenBrainzSourceConfig"},"ListenBrainzSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the ListenBrainz server, if not using the default","examples":["https://api.listenbrainz.org/"],"default":"https://api.listenbrainz.org/"},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","username"],"title":"ListenBrainzSourceData"},"JRiverSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/JRiverData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"JRiverSourceConfig"},"JRiverData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the JRiver HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `52199`\n* Path => `/MCWS/v1/`","examples":["http://localhost:52199/MCWS/v1/"],"default":"http://localhost:52199/MCWS/v1/"},"username":{"type":"string","description":"If you have enabled authentication, the username you set"},"password":{"type":"string","description":"If you have enabled authentication, the password you set"}},"required":["url"],"title":"JRiverData"},"KodiSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KodiData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"KodiSourceConfig"},"KodiData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`","examples":["http://localhost:8080/jsonrpc"],"default":"http://localhost:8080/jsonrpc"},"username":{"type":"string","description":"The username set for Remote Control via Web Sever"},"password":{"type":"string","description":"The password set for Remote Control via Web Sever"}},"required":["url","username","password"],"title":"KodiData"},"WebScrobblerSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/WebScrobblerData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"title":"WebScrobblerSourceConfig"},"WebScrobblerData":{"type":"object","properties":{"slug":{"type":["string","null"],"description":"The URL ending that should be used to identify scrobbles for this source\n\nIn WebScrobbler's Webhook you must set an 'API URL'. All MS WebScrobbler sources must start like:\n\nhttp://localhost:9078/api/webscrobbler\n\nIf you are using multiple WebScrobbler sources (scrobbles for many users) you must use a slug to match Sources with each users extension.\n\nExample:\n\n* slug: 'usera' => API URL: http://localhost:9078/api/webscrobbler/usera\n* slug: 'userb' => API URL: http://localhost:9078/api/webscrobbler/userb\n\nIf no slug is found from an extension's incoming webhook event the first WebScrobbler source without a slug will be used"},"blacklist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Block scrobbling from specific WebScrobbler Connectors","examples":[["youtube"]]},"whitelist":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only allow scrobbling from specific WebScrobbler Connectors","examples":[["mixcloud","soundcloud","bandcamp"]]}},"title":"WebScrobblerData"},"ChromecastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/ChromecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"ChromecastSourceConfig"},"ChromecastData":{"type":"object","properties":{"blacklistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any cast devices that START WITH these values, case-insensitive\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"whitelistDevices":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any cast device that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored\n\nUseful when used with auto discovery","examples":[["home-mini","family-tv"]]},"blacklistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"DO NOT scrobble from any application that START WITH these values, case-insensitive","examples":[["spotify","pandora"]]},"whitelistApps":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ONLY scrobble from any application that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored","examples":[["spotify","pandora"]]},"useAvahi":{"type":"boolean","description":"Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying\n\nUseful for docker (alpine) container where mDNS resolution is not yet supported. Avahi socket must be exposed to the container and avahi-tools must be installed.","default":false},"useAutoDiscovery":{"type":"boolean","description":"Use mDNS to discovery Google Cast devices on your next automatically?\n\nIf not explicitly set then it is TRUE if `devices` is not set"},"devices":{"type":"array","items":{"$ref":"#/definitions/ChromecastDeviceInfo"},"description":"A list of Google Cast devices to monitor\n\nIf this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set"},"allowUnknownMedia":{"anyOf":[{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"description":"Chromecast Apps report a \"media type\" in the status info returned for whatever is currently playing\n\n* If set to TRUE then Music AND Generic/Unknown media will be tracked for ALL APPS\n* If set to FALSE then only media explicitly typed as Music will be tracked for ALL APPS\n* If set to a list then only Apps whose name contain one of these values, case-insensitive, will have Music AND Generic/Unknown tracked\n\nSee https://developers.google.com/cast/docs/media/messages#MediaInformation \"metadata\" property","default":false},"forceMediaRecognitionOn":{"type":"array","items":{"type":"string"},"description":"Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \"media type\" reported\n\nApps will be recognized if they CONTAIN any of these values, case-insensitive"}},"title":"ChromecastData"},"ChromecastDeviceInfo":{"type":"object","properties":{"name":{"type":"string","description":"A friendly name to identify this device","examples":["MySmartTV"]},"address":{"type":"string","description":"The IP address of the device","examples":["192.168.0.115"]}},"required":["name","address"],"title":"ChromecastDeviceInfo"},"MalojaSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MalojaSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"MalojaSourceConfig"},"MalojaSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for maloja server","examples":["http://localhost:42010"]},"apiKey":{"type":"string","description":"API Key for Maloja server","examples":["myApiKey"]}},"required":["apiKey","url"],"title":"MalojaSourceData"},"MusikcubeSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusikcubeData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MusikcubeSourceConfig"},"MusikcubeData":{"type":"object","properties":{"url":{"type":"string","description":"URL of the Musikcube Websocket (Metadata) server to connect to\n\nYou MUST have enabled 'metadata' server and set a password: https://github.com/clangen/musikcube/wiki/remote-api-documentation\n * musikcube -> settings -> server setup\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `ws://`\n* Hostname => `localhost`\n* Port => `7905`","examples":["ws://localhost:7905"],"default":"ws://localhost:7905"},"password":{"type":"string","description":"Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation\n\n* musikcube -> settings -> server setup -> password"},"device_id":{"type":"string"}},"required":["password"],"title":"MusikcubeData"},"MusicCastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MusicCastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"MusicCastSourceConfig"},"MusicCastData":{"type":"object","properties":{"url":{"type":"string","description":"The host or URL of the YamahaExtendedControl endpoint to use","examples":[["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]]}},"required":["url"],"title":"MusicCastData"},"MPDSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/MPDData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/MPDSourceOptions"}},"required":["data","options"],"title":"MPDSourceConfig"},"MPDData":{"type":"object","properties":{"url":{"type":"string","description":"URL:PORT of the MPD server to connect to\n\nTo use this you must have TCP connections enabled for your MPD server https://mpd.readthedocs.io/en/stable/user.html#client-connections","examples":["localhost:6600"],"default":"localhost:6600"},"path":{"type":"string","description":"If using socket specify the path instead of url.\n\ntrailing `~` is replaced by your home directory"},"password":{"type":"string","description":"Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords"}},"title":"MPDData"},"MPDSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"MPDSourceOptions"},"VLCSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/VLCData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/VLCSourceOptions"}},"required":["data"],"title":"VLCSourceConfig"},"VLCData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"url":{"type":"string","description":"URL:PORT of the VLC server to connect to\n\nTo use this you must have the Web (http) interface module enabled and a password set https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc","examples":["localhost:8080"],"default":"localhost:8080"},"password":{"type":"string","description":"Password for the server"}},"required":["password"],"title":"VLCData"},"VLCSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"filenamePatterns":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"A list of regular expressions to use to extract metadata (title, album, artist) from a filename\n\nUsed when VLC reports only the filename for the current audio track"},"logFilenamePatterns":{"type":"boolean","description":"Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns","default":false},"dumpVlcMetadata":{"type":"boolean","description":"Dump all the metadata VLC reports for an audio track to DEBUG.\n\nUse this if reporting an issue with VLC not correctly capturing metadata for a track.","default":false}},"title":"VLCSourceOptions"},"IcecastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/IcecastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/IcecastSourceOptions"}},"required":["data"],"title":"IcecastSourceConfig"},"IcecastData":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/definitions/IcecastSource"}},"icestatsEndpoint":{"type":"string"},"statsEndpoint":{"type":"string"},"nextsongsEndpoint":{"type":"string"},"sevenhtmlEndpoint":{"type":"string"},"icyMetaInt":{"type":"number"},"url":{"type":"string","description":"The Icecast stream URL"}},"required":["url"],"title":"IcecastData"},"IcecastSource":{"type":"string","enum":["icy","ogg","icestats","stats","sevenhtml","nextsongs"],"title":"IcecastSource"},"IcecastSourceOptions":{"type":"object","properties":{"systemScrobble":{"type":"boolean","description":"For Sources that support manual listening, should MS default to scrobbling when no user interaction has occurred?\n\nIf not specified MS will use a Source's specific behavior, see Source's documentation."},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"IcecastSourceOptions"},"AzuracastSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/AzuracastData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"}},"required":["data"],"title":"AzuracastSourceConfig"},"AzuracastData":{"type":"object","properties":{"url":{"type":"string","description":"Base URL of the Azuracast instance\n\nThis does NOT include the station. If a station is included it will be ignored. Use `station` field to specify station, if necessary","examples":["https://radio.mydomain.tld","http://localhost:80"]},"station":{"type":"string","description":"The specific station to monitor\n\nScrobbling will only occur if any of the monitor conditions are met AND the station is ONLINE.\n\nTo monitor multiple stations create a Source for each station.","examples":["my-station-1"]},"monitorWhenListeners":{"type":["boolean","number"],"description":"Only activate scrobble monitoring if station\n\n* `true` => has any current listeners\n* `number` => has EQUAL TO or MORE THAN X number of listeners"},"monitorWhenLive":{"type":"boolean","description":"Only activate scrobble monitoring if station has a live DJ/Streamer","default":true},"apiKey":{"type":"string","description":"API Key used to access data about private streams\n\nhttps://www.azuracast.com/docs/developers/apis/#api-authentication"}},"required":["url","station"],"title":"AzuracastData"},"KoitoSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/KoitoSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/CommonSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"KoitoSourceConfig"},"KoitoSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"url":{"type":"string","description":"URL for the Koito server","examples":["http://192.168.0.100:4110"]},"token":{"type":"string","description":"User token for the user to scrobble for","examples":["pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL"]},"username":{"type":"string","description":"Username of the user to scrobble for"}},"required":["token","url","username"],"title":"KoitoSourceData"},"TealSourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/TealSourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/TealSourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"Should always be `souce` when using Tealfm as a Source","default":"source","examples":["source"]}},"required":["data"],"title":"TealSourceConfig"},"TealSourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"baseUri":{"type":"string","description":"The base URI of the Multi-Scrobbler to use for ATProto OAuth\n\nOnly include this if you want to use OAuth. The URI must be a non-IP/non-local domain using https: protocol."},"identifier":{"type":"string","description":"Identify the account to login as\n\n* For **App Password** Auth - your email\n* For **Oauth** - your handle minus the @"},"appPassword":{"type":"string","description":"The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account\n\nThis is created under https://bsky.app/settings/app-passwords\n\n**Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT**"},"serviceAllow":{"type":"array","items":{"type":"string"}},"serviceDeny":{"type":"array","items":{"type":"string"}}},"required":["identifier"],"title":"TealSourceData"},"TealSourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"}},"title":"TealSourceOptions"},"RockskySourceConfig":{"type":"object","properties":{"name":{"type":"string","description":"Unique identifier for this source."},"data":{"$ref":"#/definitions/RockskySourceData"},"enable":{"type":"boolean","description":"Should MS use this client/source? Defaults to true","default":true,"examples":[true]},"clients":{"type":"array","items":{"type":"string"},"description":"Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.","examples":[["MyMalojaConfigName","MyLastFMConfigName"]]},"options":{"$ref":"#/definitions/RockskySourceOptions"},"configureAs":{"type":"string","enum":["source"],"description":"When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.","default":"source","examples":["source"]}},"required":["data"],"title":"RockskySourceConfig"},"RockskySourceData":{"type":"object","properties":{"interval":{"type":"number","description":"How long to wait before polling the source API for new tracks (in seconds)","default":10,"examples":[10]},"maxInterval":{"type":"number","description":"When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)","default":30,"examples":[30]},"staleAfter":{"type":"number","description":"Number of seconds after which A Player is considered Stale\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Stale. When the Player becomes Stale:\n\n* The current listening session is ended. If the Player becomes active again a new listening session is started (Player will miss `interval` seconds of listening)\n* If the player has an existing session w/ track then MS attempts to scrobble it\n\nThis option DOES NOT need to be set. It is automatically calculated as (`interval` * 3) when not defined."},"orphanedAfter":{"type":"number","description":"Number of seconds after which A Player is considered Orphaned\n\nWhen Polling the source does not recieve data about a specific Player after X seconds it becomes Orphaned. When the Player becomes Orphaned:\n\n* The current Player session is ended and the Player is removed from MS\n* MS attempts to scrobble, if the Player has an existing session w/ track\n\nA Player should become Orphaned EQUAL TO OR AFTER it becomes Stale.\n\n* This option DOES NOT need to be set. It is automatically calculated as (`interval` * 5) when not defined.\n* If it is set it must be equal to or larger than `staleAfter` or (`interval * 3`)"},"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"key":{"type":"string","description":"API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account","examples":["6794186bf-1157-4de6-80e5-uvb411f3ea2b"]},"handle":{"type":"string","description":"The **fully-qualified** handle for your ATPRoto/Bluesky account, like:\n\n* alice.bsky.social\n* foxxmd.com\n* mysuer.blacksky.app"}},"required":["handle","key"],"title":"RockskySourceData"},"RockskySourceOptions":{"type":"object","properties":{"maxRequestRetries":{"type":"number","description":"default # of http request retries a source/client can make before error is thrown","default":1,"examples":[1]},"retryMultiplier":{"type":"number","description":"default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)","default":1.5,"examples":[1.5]},"maxPollRetries":{"type":"number","description":"default # of automatic polling restarts on error","default":5,"examples":[5]},"logPayload":{"type":"boolean","description":"* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload) then setting this option to true will make MS log the payload JSON to DEBUG output\n* If this source is POLLING then it will log the raw data for each unique track/response the first time it is seen","default":false,"examples":[false]},"logFilterFailure":{"type":["boolean","string"],"enum":[false,"debug","warn"],"description":"If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)\n\n* `false` => do not log\n* `debug` => log to DEBUG level\n* `warn` => log to WARN level (default)\n\nHint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.","default":"warn","examples":["warn"]},"logPlayerState":{"type":"boolean","description":"For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output","default":false,"examples":[false]},"logToFile":{"anyOf":[{"type":"boolean","enum":[true]},{"$ref":"#/definitions/LogLevel"},{"$ref":"#/definitions/FileLogOptions"}],"description":"**Exprimental:** Log to a separate file for this Source.\n\nUseful for debugging long-running Sources"},"scrobbleBacklog":{"type":"boolean","description":"If this source\n\n* supports fetching a listen history\n* and this option is enabled\n\nthen on startup MS will attempt to scrobble the recent listens from that history","default":true,"examples":[true,false]},"scrobbleThresholds":{"$ref":"#/definitions/ScrobbleThresholds","description":"Set thresholds for when multi-scrobbler should consider a tracked play to be \"scrobbable\". If both duration and percent are defined then if either condition is met the track is scrobbled."},"scrobbleBacklogCount":{"type":"number","description":"The number of listens to fetch when scrobbling from backlog\n\n* Only applies if this source supports fetching a listen history\n* If not specified it defaults to the maximum number of listens the source API supports"},"playTransform":{"$ref":"#/definitions/PlayTransformOptions"},"audioScrobblerUrl":{"type":"string","description":"URL for the Rocksky *Listenbrainz* endpoint, if not using the default","examples":["https://audioscrobbler.rocksky.app"],"default":"https://audioscrobbler.rocksky.app"},"apiUrl":{"type":"string","description":"URL for the Rocksky *API* endpoint, if not using the default","examples":["https://api.rocksky.app"],"default":"https://api.rocksky.app"}},"title":"RockskySourceOptions"}}}
\ No newline at end of file
diff --git a/src/backend/common/vendor/LastfmApiClient.ts b/src/backend/common/vendor/LastfmApiClient.ts
index 3992cad8..67d3bb0c 100644
--- a/src/backend/common/vendor/LastfmApiClient.ts
+++ b/src/backend/common/vendor/LastfmApiClient.ts
@@ -1,22 +1,18 @@
import dayjs, { Dayjs } from "dayjs";
-import LastFm, {
- AuthGetSessionResponse,
- LastfmTrackUpdateRequest, TrackObject,
- TrackScrobblePayload,
- UserGetInfoResponse
-} from "lastfm-node-client";
-import { BrainzMeta, PlayObject } from "../../../core/Atomic.js";
+import { BrainzMeta, PlayObject, URLData } from "../../../core/Atomic.js";
import { nonEmptyStringOrDefault, splitByFirstFound } from "../../../core/StringUtils.js";
import { removeUndefinedKeys, sleep, writeFile } from "../../utils.js";
import { objectIsEmpty, readJson } from '../../utils/DataUtils.js';
-import { joinedUrl } from "../../utils/NetworkUtils.js";
+import { isPortReachableConnect, joinedUrl, normalizeWebAddress } from "../../utils/NetworkUtils.js";
import { getScrobbleTsSOCDate } from "../../utils/TimeUtils.js";
import { getNodeNetworkException, isNodeNetworkException } from "../errors/NodeErrors.js";
import { UpstreamError } from "../errors/UpstreamError.js";
-import { AbstractApiOptions, DEFAULT_RETRY_MULTIPLIER, FormatPlayObjectOptions } from "../infrastructure/Atomic.js";
+import { AbstractApiOptions, DEFAULT_RETRY_MULTIPLIER, FormatPlayObjectOptions, InternalConfigOptional } from "../infrastructure/Atomic.js";
import { LastfmData } from "../infrastructure/config/client/lastfm.js";
import AbstractApiClient from "./AbstractApiClient.js";
import { parseArtistCredits } from "../../utils/StringUtils.js";
+import { LastFMUser, LastFMAuth, LastFMTrack, LastFMUserGetRecentTracksResponse, LastFMBooleanNumber, LastFMUpdateNowPlayingResponse, LastFMUserGetInfoResponse } from 'lastfm-ts-api';
+import clone from 'clone';
const badErrors = [
'api key suspended',
@@ -33,85 +29,61 @@ const retryErrors = [
'rate limit'
]
+export const LIBREFM_HOST = 'libre.fm';
+export const LIBREFM_PATH = '/2.0/';
+export const LASTFM_HOST = 'ws.audioscrobbler.com';
+export const LASTFM_PATH = '/2.0';
+
export default class LastfmApiClient extends AbstractApiClient {
user?: string;
declare config: LastfmData;
+ sessionKey?: string;
- constructor(name: any, config: Partial & {configDir: string, localUrl: URL}, options: AbstractApiOptions) {
- super('lastfm', name, config, options);
- const {redirectUri, apiKey, secret, session, configDir} = config;
- this.redirectUri = `${redirectUri ?? joinedUrl(config.localUrl, 'lastfm/callback').href}?state=${name}`;
- if (apiKey === undefined) {
- this.logger.warn("'apiKey' not found in config!");
- }
- this.workingCredsPath = `${configDir}/currentCreds-lastfm-${name}.json`;
- this.client = new LastFm(apiKey as string, secret, session);
- }
+ urlBase: string;
+ path: string;
+ upstreamName: string = 'Last.fm';
+
+ url: URLData;
+
+ userApi!: LastFMUser;
+ trackApi!: LastFMTrack;
- static formatPlayObj(obj: TrackObject, options: FormatPlayObjectOptions = {}): PlayObject {
+ constructor(name: any, config: Partial & {urlBase?: string}, options: AbstractApiOptions & InternalConfigOptional & {type?: string}) {
+ const {type = 'lastfm', configDir, localUrl} = options ?? {};
+ super(type, name, config, options);
const {
- artist: {
- '#text': artists,
- name: artistName,
- mbid: artistMbid,
- },
- name: title,
- album: {
- '#text': album,
- mbid: albumMbid,
- },
- duration,
- date: {
- uts: time,
- } = {},
- '@attr': {
- nowplaying = 'false',
- } = {},
- url,
- mbid,
- } = obj;
- // arbitrary decision yikes
- const artistStrings = splitByFirstFound(artists, [','], artistName === undefined || artistName.trim() === '' ? [] : [artistName]);
- let al = album;
- if(al !== undefined) {
- if(al === null) {
- al = undefined;
- } else if (al.trim() === '') {
- // lastfm may provide empty string when album data is not defined
- al = undefined;
- }
- }
- const brainz: BrainzMeta = removeUndefinedKeys({
- album: nonEmptyStringOrDefault(albumMbid),
- artist: splitByFirstFound(artistMbid, [',',';'], undefined),
- track: nonEmptyStringOrDefault(mbid)
- });
+ redirectUri,
+ apiKey,
+ secret,
+ session,
+ urlBase = `https://${LASTFM_HOST}${LASTFM_PATH}`
+ } = config;
- const play: PlayObject = {
- data: {
- artists: [...new Set(artistStrings)] as string[],
- track: title,
- album: al,
- duration,
- playDate: time !== undefined ? dayjs.unix(typeof time === 'string' ? Number.parseInt(time, 10) : time) : undefined
- },
- meta: {
- nowPlaying: nowplaying === 'true',
- mbid,
- source: 'Lastfm',
- url: {
- web: url,
- }
- }
- }
+ this.url = normalizeWebAddress(urlBase, {removeTrailingSlash: false});
+ let cbPrefix = 'lastfm';
- if(brainz !== undefined && !objectIsEmpty(brainz)) {
- play.data.meta = {
- brainz
+ if(this.url.url.host === LASTFM_HOST) {
+ this.logger.info('Using official Last.fm instance host/path');
+ cbPrefix = 'lastfm';
+ if (apiKey === undefined) {
+ this.logger.warn(`'apiKey' not found in config!`);
+ }
+ } else {
+ this.upstreamName = 'Libre.fm';
+ cbPrefix = 'librefm';
+ if(this.url.url.host === LIBREFM_HOST) {
+ this.logger.info('Using official Libre.fm instance host/path');
+ } else {
+ this.logger.info('Assuming custom Libre.fm instance');
}
}
- return play;
+
+ this.redirectUri = `${redirectUri ?? joinedUrl(localUrl, `${cbPrefix}/callback`).href}?state=${name}`;
+
+ this.logger.info(`Using ${this.url.normal} for API calls`);
+ this.logger.info(`Redirect Uri: ${this.redirectUri}`);
+ this.workingCredsPath = `${configDir}/currentCreds-${this.url.url.host === LASTFM_HOST ? 'lastfm' : 'librefm'}-${name}.json`;
}
callApi = async (func: any, retries = 0): Promise => {
@@ -121,7 +93,7 @@ export default class LastfmApiClient extends AbstractApiClient {
} = this.config;
try {
- return await func(this.client) as T;
+ return await func() as T;
} catch (e) {
const {
message,
@@ -156,65 +128,270 @@ export default class LastfmApiClient extends AbstractApiClient {
}
}
- getAuthUrl = () => `http://www.last.fm/api/auth/?api_key=${this.config.apiKey}&cb=${encodeURIComponent(this.redirectUri)}`
+ getAuthUrl = () => {
+
+ if(this.url.url.host === LASTFM_HOST) {
+ return `http://www.last.fm/api/auth/?api_key=${this.config.apiKey}&cb=${encodeURIComponent(this.redirectUri)}`;
+ }
+
+ const aUrl = new URL(this.url.url.toString());
+ aUrl.search = `?api_key=${this.config.apiKey}&cb=${encodeURIComponent(this.redirectUri)}`;
+ aUrl.pathname = '/api/auth';
+ return aUrl.toString();
+ }
authenticate = async (token: any) => {
- const sessionRes: AuthGetSessionResponse = await this.client.authGetSession({token});
+
+ const auth = new LastFMAuth(this.config.apiKey, this.config.secret, undefined, {
+ hostname: this.url.url.host,
+ path: this.url.url.pathname
+ });
+
+ const sessionRes = await auth.getSession({token});
const {
session: {
key: sessionKey,
name, // username
} = {}
} = sessionRes;
- this.client.sessionKey = sessionKey;
+ this.sessionKey = sessionKey;
+ this.user = name;
+
+ this.initApi();
await writeFile(this.workingCredsPath, JSON.stringify({
sessionKey,
+ name,
}));
}
+ protected initApi = () => {
+ this.userApi = new LastFMUser(this.config.apiKey, this.config.secret, this.sessionKey, {
+ hostname: this.url.url.host,
+ path: this.url.url.pathname
+ });
+ this.trackApi = new LastFMTrack(this.config.apiKey, this.config.secret, this.sessionKey, {
+ hostname: this.url.url.host,
+ path: this.url.url.pathname
+ });
+ }
+
initialize = async (): Promise => {
try {
const creds = await readJson(this.workingCredsPath, {throwOnNotFound: false, interpolateEnvs: false});
- const {sessionKey} = creds || {};
- if (this.client.sessionKey === undefined && sessionKey !== undefined) {
- this.client.sessionKey = sessionKey;
+ const {sessionKey, name} = creds || {};
+ this.sessionKey = sessionKey;
+ this.user = name;
+ if(this.sessionKey !== undefined) {
+ this.initApi();
}
return true;
} catch (e) {
- throw new Error('Current lastfm credentials file exists but could not be parsed', {cause: e});
+ throw new Error(`Current ${this.upstreamName} credentials file exists but could not be parsed`, {cause: e});
+ }
+ }
+
+ testConnection = async() => {
+ try {
+ await isPortReachableConnect(this.url.port, { host: this.url.url.hostname });
+ this.logger.verbose(`${this.url.url.hostname}:${this.url.port} is reachable.`);
+ return true;
+ } catch (e) {
+ const hint = e.error?.cause?.message ?? undefined;
+ throw new Error(`Could not connect to ${this.upstreamName} API server${hint !== undefined ? ` (${hint})` : ''}`, { cause: e.error ?? e });
}
}
testAuth = async () => {
- if (this.client.sessionKey === undefined) {
+ if (this.sessionKey === undefined) {
this.logger.warn('No session key found. User interaction for authentication required.');
this.logger.info(`Redirect URL that will be used on auth callback: '${this.redirectUri}'`);
- return false;
+ throw new Error('No session key found. User interaction for authentication required.');
}
try {
- const infoResp = await this.callApi((client: any) => client.userGetInfo());
- const {
- user: {
- name,
- } = {}
- } = infoResp;
- this.user = name;
+ // existing lastfm clients are ok with getting user from getInfo
+ // but libre throws an error
+ // so, if not libre allow empty user and set user from response to comply with new librefm implementation/refactor
+ const infoPayload: Record = {};
+ if(this.urlBase !== LASTFM_HOST) {
+ infoPayload.user = this.user;
+ }
+ const resp = await this.callApi(() => this.userApi.getInfo(infoPayload));
+ if(this.user === undefined) {
+ this.user = resp.user.name;
+ }
this.initialized = true;
- this.logger.info(`Client authorized for user ${name}`)
+ this.logger.info(`Client authorized for user ${this.user}`)
return true;
} catch (e) {
this.logger.error('Testing auth failed');
if(isNodeNetworkException(e)) {
- this.logger.error('Could not communicate with Last.fm API');
+ this.logger.error(`Could not communicate with ${this.upstreamName} API`);
}
throw e;
}
}
+
+ getRecentTracks = async (options: TracksFetchOptions = {}): Promise => {
+
+ let resp: LastFMUserGetRecentTracksResponse;
+ try {
+ resp = await this.callApi(() => this.userApi.getRecentTracks({
+ ...options,
+ user: this.user,
+ // sk is required if user has "Hide recent listening" enabled on their profile
+ sk: this.sessionKey,
+ extended: 1
+ }));
+
+ const {
+ recenttracks: {
+ track: list = [],
+ } = {}
+ } = resp;
+ return list.reduce((acc: any, x: any) => {
+ try {
+ const formatted = formatPlayObj(x);
+ const {
+ data: {
+ track,
+ playDate,
+ },
+ meta: {
+ mbid,
+ nowPlaying,
+ }
+ } = formatted;
+ if (nowPlaying === true) {
+ // if the track is "now playing" it doesn't get a timestamp so we can't determine when it started playing
+ // and don't want to accidentally count the same track at different timestamps by artificially assigning it 'now' as a timestamp
+ // so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway
+ this.logger.debug(`Ignoring 'now playing' track returned from ${this.upstreamName} client`, { track, mbid });
+ return acc;
+ } else if (playDate === undefined) {
+ this.logger.warn(`${this.upstreamName} recently scrobbled track did not contain a timestamp, omitting from time frame check`, { track, mbid });
+ return acc;
+ }
+ return acc.concat(formatted);
+ } catch (e) {
+ this.logger.warn(`Failed to format ${this.upstreamName} recently scrobbled track, omitting from time frame check`, { error: e.message });
+ this.logger.debug('Full api response object:');
+ this.logger.debug(x);
+ return acc;
+ }
+ }, []);
+ } catch (e) {
+ this.logger.debug(resp);
+ throw e;
+ }
+ }
+
+ scrobble = async (playObj: PlayObject): Promise => {
+ const {
+ meta: {
+ source,
+ newFromSource = false,
+ } = {}
+ } = playObj;
+
+ const scrobblePayload = playToClientPayload(playObj);
+
+ try {
+ const response = await this.callApi(() => this.trackApi.scrobble({...scrobblePayload}) as unknown as LastFMTrackScrobbleResponse);
+ const {
+ scrobbles: {
+ '@attr': {
+ accepted = 0,
+ ignored = 0,
+ code = undefined,
+ } = {},
+ scrobble: {
+ track: {
+ '#text': trackName,
+ } = {},
+ album: {
+ '#text': albumName
+ } = {},
+ timestamp,
+ ignoredMessage: {
+ code: ignoreCode,
+ '#text': ignoreMsg,
+ } = {},
+ ...rest
+ } = {}
+ } = {},
+ } = response;
+ if(code === 5) {
+ throw new UpstreamError(`${this.upstreamName} API reported daily scrobble limit exceeded!`, {showStopper: true});
+ }
+ if(ignored > 0) {
+ throw new LastFMIgnoredScrobble(`${this.upstreamName} ignored scrobble => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)} -- See https://www.last.fm/api/errorcodes for more information`, {showStopper: false});
+ }
+
+ const modifiedPlay = clone(playObj);
+ delete modifiedPlay.data.playDateCompleted;
+ modifiedPlay.data.playDate = dayjs.unix(timestamp);
+ if(trackName !== undefined) {
+ modifiedPlay.data.track = trackName;
+ }
+ if(albumName !== undefined) {
+ modifiedPlay.data.album = albumName;
+ }
+
+ return modifiedPlay;
+ // last fm has rate limits but i can't find a specific example of what that limit is. going to default to 1 scrobble/sec to be safe
+ //await sleep(1000);
+ } catch (e) {
+ if(!(e instanceof UpstreamError)) {
+ throw new UpstreamError(`Error received from ${this.upstreamName} API`, {cause: e, showStopper: true});
+ } else {
+ throw e;
+ }
+ } finally {
+ this.logger.debug({payload: scrobblePayload}, 'Raw Payload');
+ }
+ }
+
+ playingNow = async (data: PlayObject) => {
+ try {
+ const {timestamp, mbid, ...rest} = playToClientPayload(data);
+ const response = await this.callApi(() => this.trackApi.updateNowPlaying(rest));
+ const {
+ nowplaying: {
+ ignoredMessage: {
+ code: ignoreCode,
+ '#text': ignoreMsg,
+ } = {},
+ } = {}
+ } = response;
+ if (ignoreCode > 0) {
+ this.logger.warn({payload: rest}), `Service ignored this scrobble => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)} -- See https://www.last.fm/api/show/track.updateNowPlaying for more information`;
+ }
+ return response;
+ } catch (e) {
+ if (!(e instanceof UpstreamError)) {
+ throw new UpstreamError(`Error received from ${this.upstreamName} API`, {cause: e, showStopper: false});
+ } else {
+ throw e;
+ }
+ }
+ }
+}
+
+export interface TracksFetchOptions {
+ to?: string
+ from?: string
+ extended?: 0 | 1
+ page?: number
+ limit?: number
+}
+
+export class LastFMIgnoredScrobble extends UpstreamError {
+
}
-export const scrobblePayloadToPlay = (obj: LastfmTrackUpdateRequest): PlayObject => {
+export const scrobblePayloadToPlay = (obj: LastFMScrobbleRequestPayload): PlayObject => {
const {
artist,
track,
@@ -271,7 +448,7 @@ export const scrobblePayloadToPlay = (obj: LastfmTrackUpdateRequest): PlayObject
return play;
}
-export const playToClientPayload = (playObj: PlayObject): TrackScrobblePayload => {
+export const playToClientPayload = (playObj: PlayObject): LastFMScrobblePayload => {
const {
data: {
artists = [],
@@ -297,12 +474,12 @@ export const playToClientPayload = (playObj: PlayObject): TrackScrobblePayload =
artist = artists[0];
}
- const additionalRichPayload: Partial = {};
+ const additionalRichPayload: Partial = {};
if(duration !== 0) {
additionalRichPayload.duration = duration;
}
- const rawPayload: TrackScrobblePayload = {
+ const rawPayload: LastFMScrobblePayload = {
artist: artist,
track,
album,
@@ -327,4 +504,165 @@ export const playToClientPayload = (playObj: PlayObject): TrackScrobblePayload =
// then last.fm responds with an IGNORED scrobble and error code 1 (totally unhelpful)
// so remove all undefined keys from the object before passing to the api client
return removeUndefinedKeys(rawPayload);
- }
\ No newline at end of file
+ }
+
+export const formatPlayObj = (obj: LastFMTrackObject, options: FormatPlayObjectOptions & {source?: string} = {}): PlayObject => {
+ const {
+ artist: {
+ '#text': artists,
+ name: artistName,
+ mbid: artistMbid,
+ },
+ name: title,
+ album: {
+ '#text': album,
+ mbid: albumMbid,
+ },
+ duration,
+ date: {
+ uts: time,
+ } = {},
+ '@attr': {
+ nowplaying = 'false',
+ } = {},
+ url,
+ mbid,
+ } = obj;
+ const {
+ source = 'Lastfm'
+ } = options;
+ // arbitrary decision yikes
+ const artistStrings = splitByFirstFound(artists, [','], artistName === undefined || artistName.trim() === '' ? [] : [artistName]);
+ let al = album;
+ if(al !== undefined) {
+ if(al === null) {
+ al = undefined;
+ } else if (al.trim() === '') {
+ // lastfm may provide empty string when album data is not defined
+ al = undefined;
+ }
+ }
+ const brainz: BrainzMeta = removeUndefinedKeys({
+ album: nonEmptyStringOrDefault(albumMbid),
+ artist: splitByFirstFound(artistMbid, [',',';'], undefined),
+ track: nonEmptyStringOrDefault(mbid)
+ });
+
+ const play: PlayObject = {
+ data: {
+ artists: [...new Set(artistStrings)] as string[],
+ track: title,
+ album: al,
+ duration,
+ playDate: time !== undefined ? dayjs.unix(typeof time === 'string' ? Number.parseInt(time, 10) : time) : undefined
+ },
+ meta: {
+ nowPlaying: nowplaying === 'true',
+ mbid,
+ source,
+ url: {
+ web: url,
+ }
+ }
+ }
+
+ if(brainz !== undefined && !objectIsEmpty(brainz)) {
+ play.data.meta = {
+ brainz
+ }
+ }
+ return play;
+}
+
+type LastFMTrackScrobbleResponse = Readonly<{
+ scrobbles: {
+ scrobble: {
+ track: {
+ corrected: LastFMBooleanNumber;
+ '#text': string;
+ };
+ artist: {
+ corrected: LastFMBooleanNumber;
+ '#text': string;
+ };
+ album: {
+ corrected: LastFMBooleanNumber;
+ '#text': string;
+ };
+ albumArtist: {
+ corrected: LastFMBooleanNumber;
+ '#text': string;
+ };
+ timestamp: number;
+ ignoredMessage: {
+ code: number;
+ '#text': string;
+ };
+ }
+ '@attr': {
+ accepted: number;
+ ignored: number;
+ code?: number;
+ };
+ };
+}>;
+
+export type LastFMTrackObject = {
+ artist: {
+ mbid?: string;
+ name?: string
+ '#text': string;
+ };
+ streamable?: LastFMBooleanNumber;
+ image?: Array<{
+ '#text': string;
+ size: string;
+ }>;
+ mbid?: string;
+ album: {
+ mbid?: string;
+ '#text': string;
+ };
+ duration?: number;
+ name: string;
+ url?: string;
+ date: {
+ uts: string;
+ '#text'?: string;
+ };
+ '@attr'?: {
+ nowplaying: 'true' | 'false';
+ };
+}
+
+export interface LastFMScrobblePayload {
+ /**
+ * join multiple artists with ', '
+ * */
+ artist: string
+
+ /**
+ * track title
+ * */
+ track: string
+
+ /**
+ * Unix timestamp of time track should be scrobbled at
+ * */
+ timestamp: number
+ /**
+ * length of track in seconds
+ * */
+ duration?: number
+
+ album?: string
+
+ albumArtist?: string
+
+ /** MusicBrainz track ID */
+ mbid?: string
+}
+
+export interface LastFMScrobbleRequestPayload extends LastFMScrobblePayload {
+ method: string
+}
\ No newline at end of file
diff --git a/src/backend/index.ts b/src/backend/index.ts
index 9bd1b81d..2b51177c 100644
--- a/src/backend/index.ts
+++ b/src/backend/index.ts
@@ -97,12 +97,14 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`)
createVegaGenerator()
initLogger.info('Schema definitions generated');
- const scrobbleClients = new ScrobbleClients(root.get('clientEmitter'), root.get('sourceEmitter'), root.get('localUrl'), root.get('configDir'), root.get('logger'));
- const scrobbleSources = new ScrobbleSources(root.get('sourceEmitter'), {
+ const internalConfigOptional = {
localUrl: root.get('localUrl'),
configDir: root.get('configDir'),
version: root.get('version')
- }, root.get('logger'));
+ };
+
+ const scrobbleClients = new ScrobbleClients(root.get('clientEmitter'), root.get('sourceEmitter'), internalConfigOptional, root.get('logger'));
+ const scrobbleSources = new ScrobbleSources(root.get('sourceEmitter'), internalConfigOptional, root.get('logger'));
await root.items.cache().init();
diff --git a/src/backend/scrobblers/LastfmScrobbler.ts b/src/backend/scrobblers/LastfmScrobbler.ts
index 27bf9808..bc08f597 100644
--- a/src/backend/scrobblers/LastfmScrobbler.ts
+++ b/src/backend/scrobblers/LastfmScrobbler.ts
@@ -1,13 +1,12 @@
import { Logger } from "@foxxmd/logging";
import EventEmitter from "events";
-import { NowPlayingResponse, TrackScrobbleResponse, UserGetRecentTracksResponse } from "lastfm-node-client";
import { PlayObject } from "../../core/Atomic.js";
import { buildTrackString, capitalize } from "../../core/StringUtils.js";
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
import { UpstreamError } from "../common/errors/UpstreamError.js";
-import { FormatPlayObjectOptions } from "../common/infrastructure/Atomic.js";
+import { FormatPlayObjectOptions, InternalConfigOptional } from "../common/infrastructure/Atomic.js";
import { LastfmClientConfig } from "../common/infrastructure/config/client/lastfm.js";
-import LastfmApiClient, { playToClientPayload } from "../common/vendor/LastfmApiClient.js";
+import LastfmApiClient, { LastFMIgnoredScrobble, playToClientPayload, formatPlayObj, LASTFM_HOST, LASTFM_PATH } from "../common/vendor/LastfmApiClient.js";
import { Notifiers } from "../notifier/Notifiers.js";
import AbstractScrobbleClient, { nowPlayingUpdateByPlayDuration } from "./AbstractScrobbleClient.js";
@@ -16,13 +15,13 @@ export default class LastfmScrobbler extends AbstractScrobbleClient {
api: LastfmApiClient;
requiresAuth = true;
requiresAuthInteraction = true;
+ upstreamType: string = 'Last.fm';
declare config: LastfmClientConfig;
- constructor(name: any, config: LastfmClientConfig, options = {}, notifier: Notifiers, emitter: EventEmitter, logger: Logger) {
- super('lastfm', name, config, notifier, emitter, logger);
- // @ts-expect-error sloppy data structure assign
- this.api = new LastfmApiClient(name, config.data, {...options, logger})
+ constructor(name: any, config: LastfmClientConfig, options: InternalConfigOptional & {[key: string]: any}, notifier: Notifiers, emitter: EventEmitter, logger: Logger, type = 'lastfm') {
+ super(type, name, config, notifier, emitter, logger);
+ this.api = new LastfmApiClient(name, config.data, {...options, logger});
// https://www.last.fm/api/show/user.getRecentTracks
this.MAX_INITIAL_SCROBBLES_FETCH = 100;
this.supportsNowPlaying = true;
@@ -30,72 +29,35 @@ export default class LastfmScrobbler extends AbstractScrobbleClient {
this.nowPlayingMaxThreshold = nowPlayingUpdateByPlayDuration;
}
- formatPlayObj = (obj: any, options: FormatPlayObjectOptions = {}) => LastfmApiClient.formatPlayObj(obj, options);
+ formatPlayObj = (obj: any, options: FormatPlayObjectOptions = {}) => formatPlayObj(obj, options);
protected async doBuildInitData(): Promise {
await this.api.initialize();
return true;
}
+ protected async doCheckConnection(): Promise {
+ try {
+ await this.api.testConnection();
+ return true;
+ } catch (e) {
+ throw e;
+ }
+ }
+
doAuthentication = async () => {
try {
return await this.api.testAuth();
} catch (e) {
if(isNodeNetworkException(e)) {
- this.logger.error('Could not communicate with Last.fm API');
+ this.logger.error(`Could not communicate with ${this.upstreamType} API`);
}
throw e;
}
}
getScrobblesForRefresh = async (limit: number) => {
- const resp = await this.api.callApi((client: any) => client.userGetRecentTracks({
- user: this.api.user,
- sk: this.api.client.sessionKey,
- limit: limit,
- extended: true
- }));
- try {
- const {
- recenttracks: {
- track: list = [],
- } = {}
- } = resp;
- return list.reduce((acc: any, x: any) => {
- try {
- const formatted = LastfmApiClient.formatPlayObj(x);
- const {
- data: {
- track,
- playDate,
- },
- meta: {
- mbid,
- nowPlaying,
- }
- } = formatted;
- if (nowPlaying === true) {
- // if the track is "now playing" it doesn't get a timestamp so we can't determine when it started playing
- // and don't want to accidentally count the same track at different timestamps by artificially assigning it 'now' as a timestamp
- // so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway
- this.logger.debug("Ignoring 'now playing' track returned from Last.fm client", { track, mbid });
- return acc;
- } else if (playDate === undefined) {
- this.logger.warn(`Last.fm recently scrobbled track did not contain a timestamp, omitting from time frame check`, { track, mbid });
- return acc;
- }
- return acc.concat(formatted);
- } catch (e) {
- this.logger.warn('Failed to format Last.fm recently scrobbled track, omitting from time frame check', { error: e.message });
- this.logger.debug('Full api response object:');
- this.logger.debug(x);
- return acc;
- }
- }, []);
- } catch (e) {
- this.logger.debug(resp);
- throw e;
- }
+ return await this.api.getRecentTracks({limit});
}
cleanSourceSearchTitle = (playObj: PlayObject) => {
@@ -114,6 +76,7 @@ export default class LastfmScrobbler extends AbstractScrobbleClient {
}
doScrobble = async (playObj: PlayObject) => {
+
const {
meta: {
source,
@@ -123,84 +86,35 @@ export default class LastfmScrobbler extends AbstractScrobbleClient {
const sType = newFromSource ? 'New' : 'Backlog';
- const scrobblePayload = playToClientPayload(playObj);
-
try {
- const response = await this.api.callApi((client: any) => client.trackScrobble(
- scrobblePayload));
- const {
- scrobbles: {
- '@attr': {
- accepted = 0,
- ignored = 0,
- code = undefined,
- } = {},
- scrobble: {
- track: {
- '#text': trackName,
- } = {},
- timestamp,
- ignoredMessage: {
- code: ignoreCode,
- '#text': ignoreMsg,
- } = {},
- ...rest
- } = {}
- } = {},
- } = response;
- if(code === 5) {
- throw new UpstreamError('LastFM API reported daily scrobble limit exceeded! 😬 Disabling client', {showStopper: true});
- }
- if (newFromSource) {
- this.logger.info(`Scrobbled (New) => (${source}) ${buildTrackString(playObj)}`);
+ const respPlay = await this.api.scrobble(playObj);
+ if (newFromSource) {
+ this.logger.info(`Scrobbled (New) => (${source}) ${buildTrackString(playObj)}`);
+ } else {
+ this.logger.info(`Scrobbled (Backlog) => (${source}) ${buildTrackString(playObj)}`);
+ }
+ return respPlay;
+ } catch (e) {
+ if(e instanceof LastFMIgnoredScrobble) {
+ await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Ignored`, message: `Failed to scrobble => ${buildTrackString(playObj)} | ${e.message}`, priority: 'warn'});
} else {
- this.logger.info(`Scrobbled (Backlog) => (${source}) ${buildTrackString(playObj)}`);
+ await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'});
}
- if(ignored > 0) {
- await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: Service ignored this scrobble 😬 => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)}`, priority: 'warn'});
- this.logger.warn(`Service ignored this scrobble 😬 => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)} -- See https://www.last.fm/api/errorcodes for more information`, {payload: scrobblePayload});
- throw new UpstreamError('LastFM ignored scrobble', {showStopper: false});
- }
-
- return this.formatPlayObj({...rest, date: { uts: timestamp}, name: trackName});
- // last fm has rate limits but i can't find a specific example of what that limit is. going to default to 1 scrobble/sec to be safe
- //await sleep(1000);
- } catch (e) {
- await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'});
- this.logger.error(`Scrobble Error (${sType})`, {playInfo: buildTrackString(playObj), payload: scrobblePayload});
+ this.logger.error({playInfo: buildTrackString(playObj), payload: playToClientPayload(playObj)}, `Scrobble Error (${sType})`);
if(!(e instanceof UpstreamError)) {
- throw new UpstreamError('Error received from LastFM API', {cause: e, showStopper: true});
+ throw new UpstreamError(`Error received from ${this.upstreamType} API`, {cause: e, showStopper: true});
} else {
throw e;
}
- } finally {
- this.logger.debug('Raw Payload: ', scrobblePayload);
}
}
doPlayingNow = async (data: PlayObject) => {
// last.fm shows Now Playing for the same time as the duration of the track being submitted
try {
- const {timestamp, mbid, ...rest} = playToClientPayload(data);
- const response = await this.api.callApi((client: any) => client.trackUpdateNowPlaying(rest));
- const {
- nowplaying: {
- ignoredMessage: {
- code: ignoreCode,
- '#text': ignoreMsg,
- } = {},
- } = {}
- } = response;
- if (ignoreCode > 0) {
- this.logger.warn(`Service ignored this scrobble 😬 => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)} -- See https://www.last.fm/api/show/track.updateNowPlaying for more information`, {payload: rest});
- }
- return response;
+ return this.api.playingNow(data);
} catch (e) {
- if (!(e instanceof UpstreamError)) {
- throw new UpstreamError('Error received from LastFM API', {cause: e, showStopper: false});
- } else {
- throw e;
- }
+ throw e;
}
}
}
diff --git a/src/backend/scrobblers/LibrefmScrobbler.ts b/src/backend/scrobblers/LibrefmScrobbler.ts
new file mode 100644
index 00000000..5525dfea
--- /dev/null
+++ b/src/backend/scrobblers/LibrefmScrobbler.ts
@@ -0,0 +1,31 @@
+import EventEmitter from "events";
+import { Logger } from "@foxxmd/logging";
+import LastfmScrobbler from "./LastfmScrobbler.js";
+import { Notifiers } from "../notifier/Notifiers.js";
+import { LibrefmClientConfig } from "../common/infrastructure/config/client/librefm.js";
+import { formatPlayObj, LIBREFM_HOST, LIBREFM_PATH } from "../common/vendor/LastfmApiClient.js";
+import { LastfmClientConfig, LastfmData } from "../common/infrastructure/config/client/lastfm.js";
+import { FormatPlayObjectOptions, InternalConfigOptional } from "../common/infrastructure/Atomic.js";
+
+export default class LibrefmScrobbler extends LastfmScrobbler {
+
+ // @ts-expect-error
+ declare config: LibrefmClientConfig;
+
+ constructor(name: any, config: LibrefmClientConfig, options: InternalConfigOptional & {[key: string]: any}, notifier: Notifiers, emitter: EventEmitter, logger: Logger) {
+ const {
+ data: {
+ urlBase = `https://${LIBREFM_HOST}${LIBREFM_PATH}`,
+ apiKey = 'anything',
+ secret = 'anything',
+ ...rest
+ } = {},
+ } = config;
+ config.data = {...(rest as LastfmData), urlBase, apiKey, secret};
+ super(name, config as LastfmClientConfig, {...options, type: 'librefm'}, notifier, emitter, logger, 'librefm');
+ this.upstreamType = 'Libre.fm';
+ }
+
+ formatPlayObj = (obj: any, options: FormatPlayObjectOptions = {}) => formatPlayObj(obj, {...options, source: 'Librefm'});
+
+}
\ No newline at end of file
diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts
index cca87864..28453995 100644
--- a/src/backend/scrobblers/ScrobbleClients.ts
+++ b/src/backend/scrobblers/ScrobbleClients.ts
@@ -2,7 +2,7 @@
import { childLogger, Logger } from '@foxxmd/logging';
import dayjs, { Dayjs } from "dayjs";
import { PlayObject, SourcePlayerObj } from "../../core/Atomic.js";
-import { ClientType, clientTypes, ConfigMeta, isClientType, REPORTED_PLAYER_STATUSES, SourceIdentifier } from "../common/infrastructure/Atomic.js";
+import { ClientType, clientTypes, ConfigMeta, InternalConfig, InternalConfigOptional, isClientType, REPORTED_PLAYER_STATUSES, SourceIdentifier } from "../common/infrastructure/Atomic.js";
import { AIOConfig } from "../common/infrastructure/config/aioConfig.js";
import { ClientAIOConfig, ClientConfig } from "../common/infrastructure/config/client/clients.js";
import { LastfmClientConfig } from "../common/infrastructure/config/client/lastfm.js";
@@ -10,7 +10,7 @@ import { ListenBrainzClientConfig } from "../common/infrastructure/config/client
import { MalojaClientConfig } from "../common/infrastructure/config/client/maloja.js";
import { WildcardEmitter } from "../common/WildcardEmitter.js";
import { Notifiers } from "../notifier/Notifiers.js";
-import { isDebugMode } from "../utils.js";
+import { isDebugMode, parseBool } from "../utils.js";
import { readJson } from '../utils/DataUtils.js';
import { joinedUrl } from "../utils/NetworkUtils.js";
import { getTypeSchemaFromConfigGenerator } from "../utils/SchemaUtils.js";
@@ -28,6 +28,8 @@ import RockskyScrobbler from './RockskyScrobbler.js';
import { RockSkyClientConfig } from '../common/infrastructure/config/client/rocksky.js';
import { CommonClientOptions } from '../common/infrastructure/config/client/index.js';
import { ExternalMetadataTerm, PlayTransformHooks } from '../common/infrastructure/Transform.js';
+import LibrefmScrobbler from './LibrefmScrobbler.js';
+import { LibrefmClientConfig } from '../common/infrastructure/config/client/librefm.js';
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
@@ -38,8 +40,8 @@ export default class ScrobbleClients {
/** @type AbstractScrobbleClient[] */
clients: (MalojaScrobbler | LastfmScrobbler | KoitoScrobbler | TealScrobbler)[] = [];
logger: Logger;
- configDir: string;
- localUrl: URL;
+
+ internalConfig: InternalConfig;
private schemaDefinitions: Record = {};
@@ -47,12 +49,14 @@ export default class ScrobbleClients {
sourceEmitter: WildcardEmitter;
- constructor(emitter: WildcardEmitter, sourceEmitter: WildcardEmitter, localUrl: URL, configDir: string, parentLogger: Logger) {
+ constructor(emitter: WildcardEmitter, sourceEmitter: WildcardEmitter, internal: InternalConfigOptional, parentLogger: Logger) {
this.emitter = emitter;
this.sourceEmitter = sourceEmitter;
- this.configDir = configDir;
- this.localUrl = localUrl;
this.logger = childLogger(parentLogger, 'Scrobblers'); // winston.loggers.get('app').child({labels: ['Scrobblers']}, mergeArr);
+ this.internalConfig = {
+ ...internal,
+ logger: this.logger
+ }
this.sourceEmitter.on('playerUpdate', async (payload: { data: SourcePlayerObj & { options: { scrobbleTo: string[] } }} & SourceIdentifier) => {
// agressively update Now Playing so scrobblers that display based on duration are mostly synced
@@ -71,6 +75,8 @@ export default class ScrobbleClients {
getByType = (type: any) => this.clients.filter(x => x.type === type)
+ getByNameAndType = (name: string, type: ClientType) => this.clients.find(x => x.name === name && x.type === type)
+
async getStatusSummary(type?: string, name?: string): Promise<[boolean, string[]]> {
let clients: AbstractScrobbleClient[] = [];
@@ -107,6 +113,9 @@ export default class ScrobbleClients {
case 'lastfm':
this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("LastfmClientConfig");
break;
+ case 'librefm':
+ this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("LibrefmClientConfig");
+ break;
case 'listenbrainz':
this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("ListenBrainzClientConfig");
break;
@@ -129,7 +138,7 @@ export default class ScrobbleClients {
let configFile;
try {
- configFile = await readJson(`${this.configDir}/config.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `Secrets`)});
+ configFile = await readJson(`${this.internalConfig.configDir}/config.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `Secrets`)});
} catch (e) {
// think this should stay as show-stopper since config could include important defaults (delay, retries) we don't want to ignore
throw new Error('config.json could not be parsed');
@@ -206,6 +215,9 @@ export default class ScrobbleClients {
secret: process.env.LASTFM_SECRET,
redirectUri: process.env.LASTFM_REDIRECT_URI,
session: process.env.LASTFM_SESSION,
+ librefm: process.env.LASTFM_LIBREFM_MODE !== undefined ? parseBool(process.env.LASTFM_LIBREFM_MODE, undefined) : undefined,
+ host: process.env.LASTFM_HOST,
+ path: process.env. LASTFM_PORT
};
if (!Object.values(lfm).every(x => x === undefined)) {
configs.push({
@@ -214,11 +226,32 @@ export default class ScrobbleClients {
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: {...lfm, redirectUri: lfm.redirectUri ?? joinedUrl(this.localUrl, 'lastfm/callback').toString()},
+ data: lfm,
options: transformPresetEnv('LASTFM')
})
}
break;
+ case 'librefm': {
+ const shouldUse = parseBool(process.env.LIBRFM_ENABLE)
+ const libre = {
+ apiKey: process.env.LIBREFM_API_KEY,
+ secret: process.env.LIBREFM_SECRET,
+ redirectUri: process.env.LIBREFM_REDIRECT_URI,
+ session: process.env.LIBREFM_SESSION,
+ urlBase: process.env.LIBREFM_URLBASE,
+ };
+ if (!Object.values(libre).every(x => x === undefined) || shouldUse) {
+ configs.push({
+ type: 'librefm',
+ name: 'unnamed-librefm',
+ source: 'ENV',
+ mode: 'single',
+ configureAs: 'client',
+ data: libre,
+ options: transformPresetEnv('LIBREFM')
+ })
+ }
+ } break;
case 'listenbrainz':
const lz = {
url: process.env.LZ_URL,
@@ -295,7 +328,7 @@ export default class ScrobbleClients {
}
let rawClientConfigs;
try {
- rawClientConfigs = await readJson(`${this.configDir}/${clientType}.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `${clientType} Secrets`)});
+ rawClientConfigs = await readJson(`${this.internalConfig.configDir}/${clientType}.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `${clientType} Secrets`)});
} catch (e) {
const errMsg = `${clientType}.json config file could not be parsed`;
this.emitter.emit('error', errMsg);
@@ -400,19 +433,22 @@ ${sources.join('\n')}`);
newClient = new MalojaScrobbler(name, ({...clientConfig, data} as unknown as MalojaClientConfig), notifier, this.emitter, this.logger);
break;
case 'lastfm':
- newClient = new LastfmScrobbler(name, {...clientConfig, data: {configDir: this.configDir, ...data} } as unknown as LastfmClientConfig, {}, notifier, this.emitter, this.logger);
+ newClient = new LastfmScrobbler(name, {...clientConfig, data } as unknown as LastfmClientConfig, this.internalConfig, notifier, this.emitter, this.logger);
+ break;
+ case 'librefm':
+ newClient = new LibrefmScrobbler(name, {...clientConfig, data } as unknown as LibrefmClientConfig, this.internalConfig, notifier, this.emitter, this.logger);
break;
case 'listenbrainz':
- newClient = new ListenbrainzScrobbler(name, {...clientConfig, data: {configDir: this.configDir, ...data} } as unknown as ListenBrainzClientConfig, {}, notifier, this.emitter, this.logger);
+ newClient = new ListenbrainzScrobbler(name, {...clientConfig, data: {configDir: this.internalConfig.configDir, ...data} } as unknown as ListenBrainzClientConfig, {}, notifier, this.emitter, this.logger);
break;
case 'koito':
- newClient = new KoitoScrobbler(name, {...clientConfig, data: {configDir: this.configDir, ...data} } as unknown as KoitoClientConfig, {}, notifier, this.emitter, this.logger);
+ newClient = new KoitoScrobbler(name, {...clientConfig, data: {configDir: this.internalConfig.configDir, ...data} } as unknown as KoitoClientConfig, {}, notifier, this.emitter, this.logger);
break;
case 'tealfm':
newClient = new TealScrobbler(name, {...clientConfig, data: {...data}} as unknown as TealClientConfig, {}, notifier, this.emitter, this.logger);
break;
case 'rocksky':
- newClient = new RockskyScrobbler(name, {...clientConfig, data: {configDir: this.configDir, ...data} } as unknown as RockSkyClientConfig, {}, notifier, this.emitter, this.logger);
+ newClient = new RockskyScrobbler(name, {...clientConfig, data: {configDir: this.internalConfig.configDir, ...data} } as unknown as RockSkyClientConfig, {}, notifier, this.emitter, this.logger);
break;
default:
break;
diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts
index feb9d6a8..942ddaa5 100644
--- a/src/backend/server/auth.ts
+++ b/src/backend/server/auth.ts
@@ -13,6 +13,10 @@ import { source } from "common-tags";
import TealScrobbler from "../scrobblers/TealfmScrobbler.js";
import { parseRegexSingle } from "@foxxmd/regex-buddy-core";
import { BlueSkyOauthApiClient } from "../common/vendor/bluesky/BlueSkyOauthApiClient.js";
+import LibrefmScrobbler from "../scrobblers/LibrefmScrobbler.js";
+import LibrefmSource from "../sources/LibrefmSource.js";
+import e from "express";
+import AbstractSource from "../sources/AbstractSource.js";
export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMiddle: ExpressHandler, clientMiddle: ExpressHandler, scrobbleSources: ScrobbleSources, scrobbleClients: ScrobbleClients) => {
app.use('/api/client/auth', clientMiddle);
@@ -23,6 +27,7 @@ export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMid
switch (scrobbleClient.type) {
case 'lastfm':
+ case 'librefm':
res.redirect(scrobbleClient.api.getAuthUrl());
break;
case 'tealfm':
@@ -53,6 +58,7 @@ export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMid
}
break;
case 'lastfm':
+ case 'librefm':
res.redirect(source.api.getAuthUrl());
break;
case 'deezer':
@@ -78,23 +84,40 @@ export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMid
name
} = {}
} = req;
- if (req.url.includes('lastfm')) {
+ const entityTypeHint = req.url.includes('lastfm') ? 'lastfm' : 'librefm';
+
+ if (req.url.includes('lastfm') || req.url.includes('librefm')) {
const {
query: {
token
} = {}
} = req;
- let entity: LastfmScrobbler | LastfmSource | undefined = scrobbleClients.getByName(state) as (LastfmScrobbler | undefined);
+ let entity: LastfmScrobbler | LastfmSource | LibrefmScrobbler | LibrefmSource | undefined;
+ if(req.url.includes('lastfm')) {
+ entity = scrobbleClients.getByNameAndType(state as string, 'lastfm') as (LastfmScrobbler | LastfmSource | undefined);
+ } else {
+ entity = scrobbleClients.getByNameAndType(state as string, 'librefm') as (LibrefmScrobbler | LibrefmSource | undefined);
+ }
+
if(entity === undefined) {
- entity = scrobbleSources.getByName(state) as LastfmSource;
+ entity = scrobbleSources.getByName(state) as LastfmSource | LibrefmSource;
}
try {
+ if(entity === undefined) {
+ const error = new Error(`No source/client with type '${entityTypeHint}' and name ${state} was found`);
+ logger.error(error);
+ throw error;
+ }
await entity.api.authenticate(token);
entity.authFailure = false;
- if(entity instanceof LastfmSource) {
+ if(entity instanceof AbstractSource) {
entity.poll().catch((e) => logger.error(e));
} else {
- entity.tryInitialize().catch((e) => logger.error(e));
+ entity.tryInitialize()
+ .catch((e) => logger.error(e))
+ .then(() => {
+ entity.initScrobbleMonitoring().catch((e) => logger.error(e));
+ })
}
return res.send('OK');
} catch (e) {
diff --git a/src/backend/server/endpointLastfmRoutes.ts b/src/backend/server/endpointLastfmRoutes.ts
index 5d990f25..34a72c84 100644
--- a/src/backend/server/endpointLastfmRoutes.ts
+++ b/src/backend/server/endpointLastfmRoutes.ts
@@ -6,7 +6,7 @@ import ScrobbleSources from "../sources/ScrobbleSources.js";
import { nonEmptyBody } from "./middleware.js";
import { LFMEndpointNotifier } from "../sources/ingressNotifiers/LFMEndpointNotifier.js";
import { EndpointLastfmSource, playStateFromRequest, parseDisplayIdentifiersFromRequest } from "../sources/EndpointLastfmSource.js";
-import { LastfmTrackUpdateRequest } from "lastfm-node-client";
+import { LastFMScrobbleRequestPayload } from "../common/vendor/LastfmApiClient.js";
export const setupLastfmEndpointRoutes = (app: ExpressWithAsync, parentLogger: Logger, scrobbleSources: ScrobbleSources) => {
@@ -43,7 +43,7 @@ export const setupLastfmEndpointRoutes = (app: ExpressWithAsync, parentLogger: L
if(!('method' in req.body)) {
return res.status(400).json({error: `Missing 'method' param`});
}
- const method = (req.body as LastfmTrackUpdateRequest).method;
+ const method = (req.body as LastFMScrobbleRequestPayload).method;
if(!['track.updateNowPlaying','track.scrobble'].includes(method)) {
return res.status(400).json({error: `Unexpected 'method' param value '${method}', expected either 'track.updateNowPlaying' or 'track.scrobble'`});
}
diff --git a/src/backend/sources/EndpointLastfmSource.ts b/src/backend/sources/EndpointLastfmSource.ts
index 0dd71af4..13e4d7aa 100644
--- a/src/backend/sources/EndpointLastfmSource.ts
+++ b/src/backend/sources/EndpointLastfmSource.ts
@@ -14,8 +14,7 @@ import {
import { parseRegexSingleOrFail } from "../utils.js";
import MemorySource from "./MemorySource.js";
import { LastFMEndpointSourceConfig } from "../common/infrastructure/config/source/endpointlfm.js";
-import { LastfmTrackUpdateRequest, NowPlayingPayload, TrackScrobblePayload } from "lastfm-node-client";
-import { scrobblePayloadToPlay } from "../common/vendor/LastfmApiClient.js";
+import { LastFMScrobbleRequestPayload, scrobblePayloadToPlay } from "../common/vendor/LastfmApiClient.js";
import { Logger } from "@foxxmd/logging";
import { PlayerStateOptions } from "./PlayerState/AbstractPlayerState.js";
import { NowPlayingPlayerState } from "./PlayerState/NowPlayingPlayerState.js";
@@ -58,7 +57,7 @@ export class EndpointLastfmSource extends MemorySource {
return matchesPath;
}
- static formatPlayObj(obj: LastfmTrackUpdateRequest, options: FormatPlayObjectOptions = {}): PlayObject {
+ static formatPlayObj(obj: LastFMScrobbleRequestPayload, options: FormatPlayObjectOptions = {}): PlayObject {
return scrobblePayloadToPlay(obj);
}
@@ -85,7 +84,7 @@ export class EndpointLastfmSource extends MemorySource {
getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new NowPlayingPlayerState(logger, id, opts);
}
-export const playStateFromRequest = (obj: LastfmTrackUpdateRequest): PlayerStateData => {
+export const playStateFromRequest = (obj: LastFMScrobbleRequestPayload): PlayerStateData => {
const play = scrobblePayloadToPlay(obj);
return {
diff --git a/src/backend/sources/LastfmSource.ts b/src/backend/sources/LastfmSource.ts
index 86ea52da..8aee4fad 100644
--- a/src/backend/sources/LastfmSource.ts
+++ b/src/backend/sources/LastfmSource.ts
@@ -1,12 +1,11 @@
import dayjs from "dayjs";
import EventEmitter from "events";
-import { TrackObject, UserGetRecentTracksResponse } from "lastfm-node-client";
import request from "superagent";
import { PlayObject, SOURCE_SOT } from "../../core/Atomic.js";
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
-import { FormatPlayObjectOptions, InternalConfig, PlayPlatformId } from "../common/infrastructure/Atomic.js";
+import { FormatPlayObjectOptions, InternalConfig, PlayPlatformId, SourceType } from "../common/infrastructure/Atomic.js";
import { LastfmSourceConfig } from "../common/infrastructure/config/source/lastfm.js";
-import LastfmApiClient from "../common/vendor/LastfmApiClient.js";
+import LastfmApiClient, { formatPlayObj } from "../common/vendor/LastfmApiClient.js";
import { sortByOldestPlayDate } from "../utils.js";
import { RecentlyPlayedOptions } from "./AbstractSource.js";
import MemorySource from "./MemorySource.js";
@@ -19,10 +18,11 @@ export default class LastfmSource extends MemorySource {
api: LastfmApiClient;
requiresAuth = true;
requiresAuthInteraction = true;
+ upstreamType: string = 'Last.fm';
declare config: LastfmSourceConfig;
- constructor(name: any, config: LastfmSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
+ constructor(name: any, config: LastfmSourceConfig, internal: InternalConfig, emitter: EventEmitter, type: SourceType = 'lastfm') {
const {
data: {
interval = 15,
@@ -30,12 +30,12 @@ export default class LastfmSource extends MemorySource {
...restData
} = {}
} = config;
- super('lastfm', name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter);
+ super(type, name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter);
this.canPoll = true;
this.canBacklog = true;
this.supportsUpstreamRecentlyPlayed = true;
this.supportsUpstreamNowPlaying = true;
- this.api = new LastfmApiClient(name, {...config.data, configDir: internal.configDir, localUrl: internal.localUrl}, {logger: this.logger});
+ this.api = new LastfmApiClient(name, config.data, {logger: this.logger, type, ...internal});
this.playerSourceOfTruth = SOURCE_SOT.HISTORY;
// https://www.last.fm/api/show/user.getRecentTracks
this.SCROBBLE_BACKLOG_COUNT = 200;
@@ -43,26 +43,22 @@ export default class LastfmSource extends MemorySource {
}
static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject {
- return LastfmApiClient.formatPlayObj(obj, options);
+ return formatPlayObj(obj, options);
}
protected async doBuildInitData(): Promise {
return await this.api.initialize();
}
- protected async doCheckConnection():Promise {
+ protected async doCheckConnection(): Promise {
try {
- await request.get('http://ws.audioscrobbler.com/2.0/');
+ await this.api.testConnection();
return true;
} catch (e) {
- if(isNodeNetworkException(e)) {
- throw new Error('Could not communicate with Last.fm API server', {cause: e});
- } else if(e.status >= 500) {
- throw new Error('Last.fm API server returning an unexpected response', {cause: e})
- }
- return true;
+ throw e;
}
}
+
doAuthentication = async () => {
try {
return await this.api.testAuth();
@@ -74,48 +70,9 @@ export default class LastfmSource extends MemorySource {
getLastfmRecentTrack = async(options: RecentlyPlayedOptions = {}): Promise<[PlayObject[], PlayObject[]]> => {
const {limit = 20} = options;
- const resp = await this.api.callApi((client: any) => client.userGetRecentTracks({
- user: this.api.user,
- sk: this.api.client.sessionKey,
- limit,
- extended: true
- }));
try {
- const {
- recenttracks: {
- track: list = [],
- } = {}
- } = resp;
-
- const plays = list.reduce((acc: PlayObject[], x: TrackObject) => {
- try {
- const formatted = LastfmApiClient.formatPlayObj(x);
- const {
- data: {
- track,
- playDate,
- },
- meta: {
- mbid,
- nowPlaying,
- }
- } = formatted;
- if (playDate === undefined) {
- if (nowPlaying === true) {
- formatted.data.playDate = dayjs();
- return acc.concat(formatted);
- }
- this.logger.warn(`Last.fm recently scrobbled track did not contain a timestamp, omitting from time frame check`, { track, mbid });
- return acc;
- }
- return acc.concat(formatted);
- } catch (e) {
- this.logger.warn('Failed to format Last.fm recently scrobbled track, omitting from time frame check', { error: e.message });
- this.logger.debug('Full api response object:');
- this.logger.debug(x);
- return acc;
- }
- }, []).sort(sortByOldestPlayDate);
+ const plays = await this.api.getRecentTracks({limit});
+ plays.sort(sortByOldestPlayDate);
// if the track is "now playing" it doesn't get a timestamp so we can't determine when it started playing
// and don't want to accidentally count the same track at different timestamps by artificially assigning it 'now' as a timestamp
// so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway
@@ -123,7 +80,6 @@ export default class LastfmSource extends MemorySource {
const now = plays.filter(x => x.meta.nowPlaying === true);
return [history, now];
} catch (e) {
- this.logger.debug(resp);
throw e;
}
}
diff --git a/src/backend/sources/LibrefmSource.ts b/src/backend/sources/LibrefmSource.ts
new file mode 100644
index 00000000..6d091d25
--- /dev/null
+++ b/src/backend/sources/LibrefmSource.ts
@@ -0,0 +1,28 @@
+import { EventEmitter } from "stream";
+import { InternalConfig } from "../common/infrastructure/Atomic.js";
+import { LibrefmSourceConfig } from "../common/infrastructure/config/source/librefm.js";
+import LastfmSource from "./LastfmSource.js";
+import { LIBREFM_HOST, LIBREFM_PATH } from "../common/vendor/LastfmApiClient.js";
+import { LastfmData } from "../common/infrastructure/config/client/lastfm.js";
+import { LastfmSourceConfig } from "../common/infrastructure/config/source/lastfm.js";
+
+
+export default class LibrefmSource extends LastfmSource {
+
+ // @ts-expect-error
+ declare config: LibrefmSourceConfig;
+
+ constructor(name: any, config: LibrefmSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
+ const {
+ data: {
+ urlBase = `https://${LIBREFM_HOST}${LIBREFM_PATH}`,
+ apiKey = 'anything',
+ secret = 'anything',
+ ...rest
+ } = {},
+ } = config;
+ config.data = {...(rest as LastfmData), urlBase, apiKey, secret};
+ super(name, config as LastfmSourceConfig, internal, emitter, 'librefm');
+ this.upstreamType = 'Libre.fm';
+ }
+}
\ No newline at end of file
diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts
index 5576ff90..14c258fa 100644
--- a/src/backend/sources/ScrobbleSources.ts
+++ b/src/backend/sources/ScrobbleSources.ts
@@ -1,7 +1,7 @@
/* eslint-disable no-case-declarations */
import { childLogger, Logger } from '@foxxmd/logging';
import EventEmitter from "events";
-import { ConfigMeta, InternalConfig, isSourceType, SourceType, sourceTypes } from "../common/infrastructure/Atomic.js";
+import { ConfigMeta, InternalConfig, InternalConfigOptional, isSourceType, SourceType, sourceTypes } from "../common/infrastructure/Atomic.js";
import { AIOConfig, SourceDefaults } from "../common/infrastructure/config/aioConfig.js";
import { AzuracastData, AzuracastSourceConfig } from "../common/infrastructure/config/source/azuracast.js";
import { ChromecastSourceConfig } from "../common/infrastructure/config/source/chromecast.js";
@@ -70,13 +70,13 @@ import { RockskySourceConfig } from '../common/infrastructure/config/source/rock
import RockskySource from './RockskySource.js';
import { CommonSourceOptions } from '../common/infrastructure/config/source/index.js';
import { ExternalMetadataTerm, PlayTransformHooks, PlayTransformOptions } from '../common/infrastructure/Transform.js';
+import LibrefmSource from './LibrefmSource.js';
+import { LibrefmSourceConfig } from '../common/infrastructure/config/source/librefm.js';
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
type ParsedConfig = SourceAIOConfig & ConfigMeta;
-type InternalConfigOptional = Omit
-
export default class ScrobbleSources {
sources: AbstractSource[] = [];
@@ -162,6 +162,9 @@ export default class ScrobbleSources {
case 'lastfm':
this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("LastfmSourceConfig");
break;
+ case 'librefm':
+ this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("LibrefmSourceConfig");
+ break;
case 'ytmusic':
this.schemaDefinitions[type] = getTypeSchemaFromConfigGenerator("YTMusicSourceConfig");
break;
@@ -720,8 +723,8 @@ export default class ScrobbleSources {
continue;
}
for (const [i,rawConf] of sourceConfigs.entries()) {
- if(['lastfm','listenbrainz','koito','maloja','tealfm','rocksky'].includes(sourceType) &&
- ((rawConf as LastfmSourceConfig | ListenBrainzSourceConfig | KoitoSourceConfig | MalojaSourceConfig | TealSourceConfig | RockskySourceConfig).configureAs !== 'source'))
+ if(['lastfm','listenbrainz','koito','maloja','tealfm','rocksky','librefm'].includes(sourceType) &&
+ ((rawConf as LastfmSourceConfig | LibrefmSourceConfig | ListenBrainzSourceConfig | KoitoSourceConfig | MalojaSourceConfig | TealSourceConfig | RockskySourceConfig).configureAs !== 'source'))
{
this.logger.debug(`Skipping config ${i + 1} from ${sourceType}.json because it is configured as a client.`);
continue;
@@ -824,6 +827,9 @@ export default class ScrobbleSources {
case 'lastfm':
newSource = await new LastfmSource(name, compositeConfig as LastfmSourceConfig, this.internalConfig, this.emitter);
break;
+ case 'librefm':
+ newSource = await new LibrefmSource(name, compositeConfig as LibrefmSourceConfig, this.internalConfig, this.emitter);
+ break;
case 'deezer':
const deezerConfig = compositeConfig as DeezerCompatConfig;
if(deezerConfig.data.arl !== undefined) {
diff --git a/src/backend/sources/ingressNotifiers/LFMEndpointNotifier.ts b/src/backend/sources/ingressNotifiers/LFMEndpointNotifier.ts
index c1e3269b..e46e5f32 100644
--- a/src/backend/sources/ingressNotifiers/LFMEndpointNotifier.ts
+++ b/src/backend/sources/ingressNotifiers/LFMEndpointNotifier.ts
@@ -2,7 +2,7 @@ import { Logger } from "@foxxmd/logging";
import { Request } from "express";
import { parseIdentifiersFromRequest } from "../EndpointLastfmSource.js";
import { IngressNotifier } from "./IngressNotifier.js";
-import { LastfmTrackUpdateRequest } from "lastfm-node-client";
+import { LastFMScrobbleRequestPayload } from "../../common/vendor/LastfmApiClient.js";
export class LFMEndpointNotifier extends IngressNotifier {
@@ -40,7 +40,7 @@ export class LFMEndpointNotifier extends IngressNotifier {
if(!('method' in req.body)) {
return `Body is missing 'method' param`
}
- const method = (req.body as LastfmTrackUpdateRequest).method;
+ const method = (req.body as LastFMScrobbleRequestPayload).method;
if(!['track.updateNowPlaying','track.scrobble'].includes(method)) {
return `Unexpected 'method' param value '${method}', expected either 'track.updateNowPlaying' or 'track.scrobble'`
}
diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts
index 2799ce9a..8d4d8d66 100644
--- a/src/backend/tests/config/config.test.ts
+++ b/src/backend/tests/config/config.test.ts
@@ -90,7 +90,12 @@ describe('Sample Configs', function () {
let emitter = new EventEmitter();
await copyFile(samplePath(componentType), `${componentType}.json`);
- const clients = new ScrobbleClients(emitter, new EventEmitter, new URL('http://example.com'), process.cwd(), loggerTest);
+ const clients = new ScrobbleClients(emitter, new EventEmitter, {
+ localUrl: new URL('http://example.com'),
+ configDir: process.cwd(),
+ version: 'test'
+ },
+ loggerTest);
await clients.buildClientsFromConfig(new Notifiers(new EventEmitter, new EventEmitter, new EventEmitter, loggerTest));
expect(clients.clients).length(1);
});
diff --git a/src/backend/tests/lastfm/lastfm.test.ts b/src/backend/tests/lastfm/lastfm.test.ts
index e594f70d..83304af7 100644
--- a/src/backend/tests/lastfm/lastfm.test.ts
+++ b/src/backend/tests/lastfm/lastfm.test.ts
@@ -3,8 +3,7 @@ import asPromised from 'chai-as-promised';
import { after, before, describe, it } from 'mocha';
import { generateLastfmTrackObject, generateMbid, generatePlay } from "../utils/PlayTestUtils.js";
-import LastfmApiClient, { playToClientPayload } from '../../common/vendor/LastfmApiClient.js';
-import { TrackObject } from 'lastfm-node-client';
+import { playToClientPayload, formatPlayObj } from '../../common/vendor/LastfmApiClient.js';
chai.use(asPromised);
@@ -24,7 +23,7 @@ describe('#LFM Track to Play', function() {
it('Sets mbids to undefined when values are empty strings', function() {
const to = generateLastfmTrackObject();
- const play = LastfmApiClient.formatPlayObj(to);
+ const play = formatPlayObj(to);
expect(play.data.meta?.brainz?.album).to.be.undefined;
expect(play.data.meta?.brainz?.artist).to.be.undefined;
@@ -35,15 +34,15 @@ describe('#LFM Track to Play', function() {
const toAlbum = generateLastfmTrackObject();
toAlbum.album.mbid = generateMbid();
- expect(LastfmApiClient.formatPlayObj(toAlbum).data.meta?.brainz?.album).to.not.be.undefined;
+ expect(formatPlayObj(toAlbum).data.meta?.brainz?.album).to.not.be.undefined;
const toArtist = generateLastfmTrackObject();
toArtist.artist.mbid = generateMbid();
- expect(LastfmApiClient.formatPlayObj(toArtist).data.meta?.brainz?.artist).to.not.be.undefined;
+ expect(formatPlayObj(toArtist).data.meta?.brainz?.artist).to.not.be.undefined;
const toTrack = generateLastfmTrackObject();
toTrack.mbid = generateMbid();
- expect(LastfmApiClient.formatPlayObj(toTrack).data.meta?.brainz?.track).to.not.be.undefined;
+ expect(formatPlayObj(toTrack).data.meta?.brainz?.track).to.not.be.undefined;
});
it('Sets artist correctly from #text or name', function() {
@@ -51,17 +50,17 @@ describe('#LFM Track to Play', function() {
const toArtText = generateLastfmTrackObject();
delete toArtText.artist.name;
expect(toArtText.artist['#text']).to.not.be.undefined;
- expect(LastfmApiClient.formatPlayObj(toArtText).data.artists[0]).to.eq(toArtText.artist['#text']);
+ expect(formatPlayObj(toArtText).data.artists[0]).to.eq(toArtText.artist['#text']);
const toArtTextEmptyNAme = generateLastfmTrackObject();
toArtTextEmptyNAme.artist.name = '';
expect(toArtTextEmptyNAme.artist['#text']).to.not.be.undefined;
- expect(LastfmApiClient.formatPlayObj(toArtTextEmptyNAme).data.artists[0]).to.eq(toArtTextEmptyNAme.artist['#text']);
+ expect(formatPlayObj(toArtTextEmptyNAme).data.artists[0]).to.eq(toArtTextEmptyNAme.artist['#text']);
const toArtName = generateLastfmTrackObject();
delete toArtName.artist['#text'];
expect(toArtName.artist.name).to.not.be.undefined;
- expect(LastfmApiClient.formatPlayObj(toArtName).data.artists[0]).to.eq(toArtName.artist.name);
+ expect(formatPlayObj(toArtName).data.artists[0]).to.eq(toArtName.artist.name);
});
});
\ No newline at end of file
diff --git a/src/backend/tests/utils/PlayTestUtils.ts b/src/backend/tests/utils/PlayTestUtils.ts
index 3c3db98b..96cc37cc 100644
--- a/src/backend/tests/utils/PlayTestUtils.ts
+++ b/src/backend/tests/utils/PlayTestUtils.ts
@@ -10,9 +10,9 @@ import { sortByNewestPlayDate } from "../../utils.js";
import { NO_DEVICE, NO_USER, PlayerStateDataMaybePlay, PlayPlatformId, ReportedPlayerStatus } from '../../common/infrastructure/Atomic.js';
import { arrayListAnd } from '../../../core/StringUtils.js';
import { findDelimiters } from '../../utils/StringUtils.js';
-import { TrackObject } from 'lastfm-node-client';
import { ListRecord, ScrobbleRecord } from '../../common/infrastructure/config/client/tealfm.js';
import { nanoid } from 'nanoid';
+import { LastFMTrackObject } from '../../common/vendor/LastfmApiClient.js';
dayjs.extend(utc)
dayjs.extend(isBetween);
@@ -343,7 +343,7 @@ export const generateArtistsStr = (options: CompoundArtistGenerateOptions = {}):
return [artistStr, primaryArt, secondaryArt];
}
-export const generateLastfmTrackObject = (): TrackObject => {
+export const generateLastfmTrackObject = (): LastFMTrackObject => {
const now = dayjs();
const artist = faker.music.artist();
return {
diff --git a/src/backend/utils/NetworkUtils.ts b/src/backend/utils/NetworkUtils.ts
index bf3b0f45..1cb8326e 100644
--- a/src/backend/utils/NetworkUtils.ts
+++ b/src/backend/utils/NetworkUtils.ts
@@ -87,16 +87,16 @@ export const isPortReachableConnect = async (port: number, opts: PortReachableOp
const QUOTES_UNWRAP_REGEX: RegExp = new RegExp(/^"(.*)"$/);
-export const normalizeWebAddress = (val: string, options: {defaultPath?: string} = {}): URLData => {
+export const normalizeWebAddress = (val: string, options: {defaultPath?: string, removeTrailingSlash?: boolean} = {}): URLData => {
let cleanUserUrl = val.trim();
const results = parseRegexSingle(QUOTES_UNWRAP_REGEX, val);
if (results !== undefined && results.groups && results.groups.length > 0) {
cleanUserUrl = results.groups[0];
}
- const {defaultPath} = options;
+ const {defaultPath, removeTrailingSlash = true} = options;
- let normal = normalizeUrl(cleanUserUrl, {removeTrailingSlash: true});
+ let normal = normalizeUrl(cleanUserUrl, {removeTrailingSlash});
const u = new URL(normal);
let port: number;
diff --git a/src/client/components/statusCard/ClientStatusCard.tsx b/src/client/components/statusCard/ClientStatusCard.tsx
index 3909714c..5432891c 100644
--- a/src/client/components/statusCard/ClientStatusCard.tsx
+++ b/src/client/components/statusCard/ClientStatusCard.tsx
@@ -58,7 +58,6 @@ const ClientStatusCard = (props: ClientStatusCardData) => {
authed,
initialized
} = data;
- if(type === 'lastfm' || type === 'listenbrainz')
header = `${display} (Client)`;
const scrobbled = initialized && (!hasAuth || (hasAuth && authed)) ? Tracks Scrobbled : Tracks Scrobbled;
diff --git a/src/client/components/statusCard/SourceStatusCard.tsx b/src/client/components/statusCard/SourceStatusCard.tsx
index aab1d1ce..4df93f51 100644
--- a/src/client/components/statusCard/SourceStatusCard.tsx
+++ b/src/client/components/statusCard/SourceStatusCard.tsx
@@ -9,6 +9,7 @@ import Player from "../player/Player";
import {useStartSourceMutation, useListenSourceMutation} from "./sourceDucks";
import './statusCard.scss';
+const ambiguousTypes = ['lastfm','listenbrainz','koito','librefm','maloja','rocksky'];
export interface SourceStatusCardData extends StatusCardSkeletonData, PropsFromRedux {
loading?: boolean
}
@@ -81,7 +82,7 @@ const SourceStatusCard = (props: SourceStatusCardData) => {
manualListening,
systemListeningBehavior
} = data;
- if(type === 'listenbrainz' || type === 'lastfm') {
+ if(ambiguousTypes.includes(type)) {
header = `${display} (Source)`;
}
diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts
index 8cae9ecb..a3a31730 100644
--- a/src/core/Atomic.ts
+++ b/src/core/Atomic.ts
@@ -13,6 +13,7 @@ export interface SourceStatusData {
| 'subsonic'
| 'jellyfin'
| 'lastfm'
+ | 'librefm'
| 'deezer'
| 'endpointlz'
| 'endpointlfm'
@@ -51,7 +52,7 @@ export interface SourceStatusData {
export interface ClientStatusData {
status: string;
- type: "maloja" | "lastfm" | "listenbrainz" | "koito" | "tealfm" | "rocksky";
+ type: "maloja" | "lastfm" | "librefm" | "listenbrainz" | "koito" | "tealfm" | "rocksky";
display: string;
scrobbled: number;
deadLetterScrobbles: number